@bitkyc08/opencodex 2.72.0 → 2.73.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-Cf-WYstE.js → App-mHGa7dE1.js} +15 -15
- package/gui/dist/assets/{Tray-DkM0mULy.js → Tray-BxY_RYZg.js} +1 -1
- package/gui/dist/assets/{index-PWrwOfio.js → index-ur_BPgK1.js} +12 -12
- package/gui/dist/assets/{quota-summary-D-FKtrgr.js → quota-summary-D1Vq7N9H.js} +1 -1
- package/gui/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/adapters/cursor/native-exec.ts +28 -10
- package/src/adapters/devin/live-models.ts +5 -0
- package/src/adapters/kiro/reasoning.ts +2 -0
- package/src/adapters/openai-chat/messages.ts +4 -1
- package/src/adapters/openai-responses/passthrough.ts +12 -1
- package/src/adapters/openai-responses/reasoning.ts +15 -0
- package/src/bridge/internal.ts +12 -0
- package/src/cli/account-anthropic-threshold.ts +41 -0
- package/src/cli/account-api.ts +2 -0
- package/src/cli/account-extended.ts +32 -21
- package/src/cli/account.ts +1 -0
- package/src/cli/capabilities.ts +8 -4
- package/src/cli/index.ts +1 -1
- package/src/codex/catalog/build-entries.ts +6 -5
- package/src/codex/catalog/derive-entry.ts +3 -4
- package/src/codex/catalog/gather-capture.ts +2 -0
- package/src/codex/catalog/metadata.ts +6 -0
- package/src/codex/catalog/native-models.ts +17 -3
- package/src/codex/catalog/parsing.ts +4 -1
- package/src/codex/catalog/pinned-models.ts +3 -1
- package/src/codex/catalog/provider-models.ts +22 -2
- package/src/codex/data/roster-pinned-models.json +178 -1
- package/src/codex/model-entitlements.ts +1 -1
- package/src/codex/native-profile-store.ts +2 -1
- package/src/combos/resolve.ts +16 -1
- package/src/config/subagent-models.ts +30 -5
- package/src/generated/compatibility-version.json +95 -67
- package/src/generated/model-metadata.ts +4 -4
- package/src/lib/account-selection-events.ts +28 -0
- package/src/lib/errors.ts +7 -5
- package/src/lib/keyring-native.ts +132 -0
- package/src/lib/provider-outbound.ts +13 -0
- package/src/lib/retry-delay.ts +10 -0
- package/src/oauth/anthropic-account-threshold.ts +13 -0
- package/src/oauth/anthropic-routing.ts +223 -52
- package/src/oauth/index.ts +4 -2
- package/src/oauth/store.ts +45 -3
- package/src/oauth/types.ts +2 -0
- package/src/providers/api-key-resolve.ts +2 -4
- package/src/providers/codebuddy-models.ts +6 -0
- package/src/providers/kiro-models.ts +4 -0
- package/src/providers/label.ts +20 -1
- package/src/providers/quota/account-cache.ts +2 -1
- package/src/providers/quota/anthropic-cooldown-recovery.ts +118 -0
- package/src/providers/quota/vendor-probes-oauth.ts +84 -57
- package/src/providers/quota.ts +45 -46
- package/src/providers/registry/entries-core.ts +3 -1
- package/src/providers/registry/entries-extended.ts +14 -1
- package/src/providers/registry/model-seeds.ts +7 -1
- package/src/server/claude-messages.ts +84 -27
- package/src/server/management/anthropic-account-threshold.ts +27 -0
- package/src/server/management/oauth-account-routes.ts +17 -4
- package/src/server/management/route-registry.ts +1 -0
- package/src/server/messages-native-oauth.ts +20 -21
- package/src/server/messages-native.ts +39 -14
- package/src/server/relay-eager.ts +5 -2
- package/src/server/relay.ts +268 -82
- package/src/server/responses/adapter-continuation.ts +6 -0
- package/src/server/responses/adapter-delivery.ts +25 -7
- package/src/server/responses/adapter-dispatch.ts +31 -0
- package/src/server/responses/buffered-sse-json.ts +345 -0
- package/src/server/responses/core-codex-account.ts +4 -1
- package/src/server/responses/core-combo.ts +28 -0
- package/src/server/responses/core-lifetime.ts +4 -0
- package/src/server/responses/passthrough-delivery.ts +305 -10
- package/src/server/responses/passthrough-dispatch.ts +60 -25
- package/src/server/responses/passthrough-error.ts +3 -1
- package/src/server/responses/request-transport.ts +22 -5
- package/src/server/responses/terminal-error-redaction.ts +53 -0
- package/src/service/windows-taskxml.ts +20 -6
- package/src/service-manager-probe.ts +115 -6
- package/src/types/wire.ts +7 -0
- package/src/usage/expected-prices.ts +11 -0
|
@@ -15,8 +15,10 @@ import type { AdapterEvent, OcxParsedRequest, OcxProviderConfig, OcxUsage } from
|
|
|
15
15
|
import type { AnthropicAccountSelectionReason } from "../../oauth/anthropic-routing";
|
|
16
16
|
import { resolveAnthropicModelRoute, routeCandidates, type AnthropicRouteDecision } from "../../oauth/anthropic-model-routes";
|
|
17
17
|
import {
|
|
18
|
+
resolveAnthropicDispatchAccountId,
|
|
18
19
|
isAnthropicAccountPoolEnabled,
|
|
19
20
|
getAnthropicPoolAccessSnapshot,
|
|
21
|
+
getAnthropicAccountHealthSnapshot,
|
|
20
22
|
getEligibleAnthropicAccounts,
|
|
21
23
|
commitAnthropicSelectionRouting,
|
|
22
24
|
formatAnthropicProviderForLog,
|
|
@@ -191,9 +193,13 @@ export async function prepareResponsesTransport(
|
|
|
191
193
|
// Resolve that choice, not the rejected candidate, before trying admission again.
|
|
192
194
|
oauthSelection = captureOAuthAccountSelection(route.providerName);
|
|
193
195
|
if (!oauthSelection) return null;
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
196
|
+
// A revision also changes on per-account policy edits. Re-evaluate the selector
|
|
197
|
+
// after credential waits even without a model route, rather than reusing stale active.
|
|
198
|
+
const revisedAnthropic = route.providerName === "anthropic"
|
|
199
|
+
? resolveAnthropicAccountForSession(anthropicSessionKey, config, Date.now(), anthropicRouteDecision) : null;
|
|
200
|
+
const revisedAnthropicId = revisedAnthropic?.accountId;
|
|
201
|
+
if (route.providerName === "anthropic" && !revisedAnthropicId) return null;
|
|
202
|
+
if (revisedAnthropic) anthropicReason = revisedAnthropic.reason;
|
|
197
203
|
candidate = route.providerName === "anthropic"
|
|
198
204
|
? await getAnthropicPoolAccessSnapshot(revisedAnthropicId ?? oauthSelection.accountId)
|
|
199
205
|
: await getValidAccessSnapshotForAccount(route.providerName, oauthSelection.accountId, { requireUsableAccount: true });
|
|
@@ -337,7 +343,8 @@ export async function prepareResponsesTransport(
|
|
|
337
343
|
const selected = captureOAuthAccountSelection(route.providerName);
|
|
338
344
|
const row = getAccountCredentialWithStatus(route.providerName, binding.snapshot.accountId);
|
|
339
345
|
return selected?.accountId === binding.selection.accountId && selected?.revision === binding.selection.revision
|
|
340
|
-
&& !!row && !row.needsReauth && row.credential.expires > Date.now()
|
|
346
|
+
&& !!row && !row.paused && !row.needsReauth && row.credential.expires > Date.now()
|
|
347
|
+
&& (route.providerName !== "anthropic" || !getAnthropicAccountHealthSnapshot(binding.snapshot.accountId))
|
|
341
348
|
&& credentialGeneration(row.credential) === binding.snapshot.generation;
|
|
342
349
|
};
|
|
343
350
|
const resolveSelectionAdapter = (provider: OcxProviderConfig, retention = config.cacheRetention): ProviderAdapter => {
|
|
@@ -399,7 +406,16 @@ export async function prepareResponsesTransport(
|
|
|
399
406
|
};
|
|
400
407
|
const refreshDispatchAdapter = async (requestParsed: OcxParsedRequest): Promise<ProviderAdapter> => {
|
|
401
408
|
if (route.provider.authMode === "oauth") {
|
|
402
|
-
|
|
409
|
+
// Pacing may outlive admission. Preserve local pause/cooldown reasons even when
|
|
410
|
+
// a strict route rejects the old proposal before another bearer can be resolved.
|
|
411
|
+
let candidate = servingOAuthSnapshot;
|
|
412
|
+
if (route.providerName === "anthropic") {
|
|
413
|
+
oauthSelection = captureOAuthAccountSelection(route.providerName);
|
|
414
|
+
const accountId = await resolveAnthropicDispatchAccountId(config, anthropicSessionKey, anthropicRouteDecision);
|
|
415
|
+
candidate = await getAnthropicPoolAccessSnapshot(accountId);
|
|
416
|
+
}
|
|
417
|
+
if (!candidate || !await applyFailoverSnapshot(candidate, requestParsed)) {
|
|
418
|
+
if (route.providerName === "anthropic") await resolveAnthropicDispatchAccountId(config, anthropicSessionKey, anthropicRouteDecision);
|
|
403
419
|
throw new Error("OAuth account selection changed before dispatch");
|
|
404
420
|
}
|
|
405
421
|
} else {
|
|
@@ -561,6 +577,7 @@ export async function prepareResponsesTransport(
|
|
|
561
577
|
anthropicRouteDecision = routeResult.decision;
|
|
562
578
|
const selection = resolveAnthropicAccountForSession(anthropicSessionKey, config, Date.now(), anthropicRouteDecision);
|
|
563
579
|
if (!selection.accountId) {
|
|
580
|
+
if (selection.reason === "paused") return formatErrorResponse(403, "permission_error", "Anthropic OAuth accounts are paused. Resume an account in account settings and retry.");
|
|
564
581
|
// Route names may resemble account IDs; log only the matched rule position.
|
|
565
582
|
if (anthropicRouteDecision) console.warn(`[anthropic-pool] route:#${anthropicRouteDecision.position} ${selection.reason}; answering locally`);
|
|
566
583
|
if (selection.reason === "all-cooled") {
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { REDACTED_SECRET, SENSITIVE_KEY_PATTERN, redactSecrets } from "../../lib/redact";
|
|
2
|
+
import { replaceSseDataPayload, sseDataPayload, type SseBlockRewrite } from "../sse-payload-rewrite";
|
|
3
|
+
|
|
4
|
+
/** Mask the selected outbound credential even when upstream echoes only its raw value. */
|
|
5
|
+
export function createOutboundCredentialMask(outboundHeaders: Record<string, string>): (text: string) => string {
|
|
6
|
+
const knownSecrets = Object.entries(outboundHeaders)
|
|
7
|
+
.filter(([name]) => SENSITIVE_KEY_PATTERN.test(name))
|
|
8
|
+
.flatMap(([name, value]) => {
|
|
9
|
+
const credential = /^(?:authorization|proxy-authorization)$/i.test(name)
|
|
10
|
+
? value.replace(/^\S+\s+/, "")
|
|
11
|
+
: value;
|
|
12
|
+
return credential ? [credential] : [];
|
|
13
|
+
})
|
|
14
|
+
.sort((a, b) => b.length - a.length);
|
|
15
|
+
return (text) => knownSecrets.reduce((safe, secret) => safe.replaceAll(secret, REDACTED_SECRET), text);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Mask upstream diagnostics before either SSE delivery or buffered JSON reconstruction. */
|
|
19
|
+
export function createTerminalErrorRedactionBlockRewrite(
|
|
20
|
+
outboundHeaders: Record<string, string>,
|
|
21
|
+
maskCredential = createOutboundCredentialMask(outboundHeaders),
|
|
22
|
+
): SseBlockRewrite {
|
|
23
|
+
const redactDiagnostic = (value: unknown): unknown => {
|
|
24
|
+
const safe = redactSecrets(value);
|
|
25
|
+
const maskKnown = (entry: unknown): unknown => {
|
|
26
|
+
if (typeof entry === "string") return maskCredential(entry);
|
|
27
|
+
if (Array.isArray(entry)) return entry.map(maskKnown);
|
|
28
|
+
if (entry && typeof entry === "object") {
|
|
29
|
+
return Object.fromEntries(Object.entries(entry).map(([key, item]) => [key, maskKnown(item)]));
|
|
30
|
+
}
|
|
31
|
+
return entry;
|
|
32
|
+
};
|
|
33
|
+
return maskKnown(safe);
|
|
34
|
+
};
|
|
35
|
+
return (block) => {
|
|
36
|
+
const terminalFrame = /^event:[ \t]*response\.(?:failed|incomplete)[ \t]*\r?$/m.test(block);
|
|
37
|
+
const payload = sseDataPayload(block);
|
|
38
|
+
if (payload === null) return [terminalFrame ? maskCredential(block) : block];
|
|
39
|
+
let event: Record<string, unknown>;
|
|
40
|
+
try {
|
|
41
|
+
event = JSON.parse(payload) as Record<string, unknown>;
|
|
42
|
+
} catch {
|
|
43
|
+
return [terminalFrame ? maskCredential(block) : block];
|
|
44
|
+
}
|
|
45
|
+
if (event?.type !== "response.failed" && event?.type !== "response.incomplete") {
|
|
46
|
+
return [terminalFrame ? maskCredential(block) : block];
|
|
47
|
+
}
|
|
48
|
+
const safeEvent = redactDiagnostic(event);
|
|
49
|
+
const rewritten = JSON.stringify(safeEvent);
|
|
50
|
+
// SSE comments and extension fields can also carry upstream text.
|
|
51
|
+
return [maskCredential(rewritten === payload ? block : replaceSseDataPayload(block, rewritten))];
|
|
52
|
+
};
|
|
53
|
+
}
|
|
@@ -427,6 +427,25 @@ function taskXmlDecodedLossyValueEquals(xml: string, tag: string, expected: stri
|
|
|
427
427
|
return taskXmlLossyValueEquals(taskXmlDecodeEntities(value), expected);
|
|
428
428
|
}
|
|
429
429
|
|
|
430
|
+
/** Accept only the single Exec action emitted for this launcher's task. */
|
|
431
|
+
export function windowsTaskActionMatches(
|
|
432
|
+
xml: string,
|
|
433
|
+
launcher: string,
|
|
434
|
+
wscript = windowsWscript(),
|
|
435
|
+
allowLossyPaths = false,
|
|
436
|
+
): boolean {
|
|
437
|
+
const scrubbed = taskXmlWithoutCommentsAndCdata(xml);
|
|
438
|
+
if (taskXmlElementCount(scrubbed, "Data") > 0 || taskXmlHasPrefixedTag(scrubbed, "Data")
|
|
439
|
+
|| taskXmlHasPrefixedTag(scrubbed, "Actions") || taskXmlHasPrefixedTag(scrubbed, "Exec")
|
|
440
|
+
|| taskXmlElementCount(scrubbed, "Actions") !== 1 || taskXmlElementCount(scrubbed, "Exec") !== 1) return false;
|
|
441
|
+
const actions = taskXmlSection(scrubbed, "Actions");
|
|
442
|
+
const exec = /<Exec(?:\s[^>]*)?>([\s\S]*?)<\/Exec>/i.exec(actions);
|
|
443
|
+
if (!exec || actions.replace(exec[0], "").trim() !== "") return false;
|
|
444
|
+
const equals = allowLossyPaths ? taskXmlDecodedLossyValueEquals : taskXmlDecodedValueEquals;
|
|
445
|
+
return equals(exec[1]!, "Command", wscript)
|
|
446
|
+
&& equals(exec[1]!, "Arguments", `/b /nologo "${launcher}"`);
|
|
447
|
+
}
|
|
448
|
+
|
|
430
449
|
export function taskXmlOptionalValueEquals(xml: string, tag: string, expected: string): boolean {
|
|
431
450
|
// Check the prefixed form first: treating `<t:Enabled>false</t:Enabled>` as an
|
|
432
451
|
// omission would turn an explicitly disabled task into a healthy one.
|
|
@@ -520,7 +539,6 @@ function windowsTaskRegistrationBaseHealthy(
|
|
|
520
539
|
const trigger = taskXmlSection(triggers, "LogonTrigger");
|
|
521
540
|
const principal = taskXmlSection(scrubbed, "Principal");
|
|
522
541
|
const settings = taskXmlSection(scrubbed, "Settings");
|
|
523
|
-
const action = taskXmlSection(scrubbed, "Exec");
|
|
524
542
|
// A self-closing <LogonTrigger /> leaves an empty section, so look for the element
|
|
525
543
|
// itself — scoped to <Triggers> so a decoy elsewhere cannot satisfy it.
|
|
526
544
|
return taskXmlElementCount(triggers, "LogonTrigger") > 0
|
|
@@ -538,11 +556,7 @@ function windowsTaskRegistrationBaseHealthy(
|
|
|
538
556
|
// cannot carry when the profile is named outside the code page (#3064). Only
|
|
539
557
|
// unrepresentable characters are forgiven; every ASCII segment and every
|
|
540
558
|
// separator is still matched literally.
|
|
541
|
-
&& (allowLossyPaths
|
|
542
|
-
? taskXmlDecodedLossyValueEquals(action, "Command", wscript)
|
|
543
|
-
&& taskXmlDecodedLossyValueEquals(action, "Arguments", `/b /nologo "${launcher}"`)
|
|
544
|
-
: taskXmlDecodedValueEquals(action, "Command", wscript)
|
|
545
|
-
&& taskXmlDecodedValueEquals(action, "Arguments", `/b /nologo "${launcher}"`));
|
|
559
|
+
&& windowsTaskActionMatches(xml, launcher, wscript, allowLossyPaths);
|
|
546
560
|
}
|
|
547
561
|
|
|
548
562
|
/** Validate the security/lifecycle-critical fields of the registered scheduler task. */
|
|
@@ -28,6 +28,10 @@ import {
|
|
|
28
28
|
} from "./lib/windows-elevation";
|
|
29
29
|
import { decodeWindowsTextBytes } from "./lib/windows-text";
|
|
30
30
|
import { WINSW_SERVICE_ID } from "./lib/winsw";
|
|
31
|
+
import { BUN_RUNTIME_PATH_ENV, BUN_RUNTIME_SOURCE_ENV } from "./lib/bun-runtime";
|
|
32
|
+
import { WINDOWS_WRAPPER_PROTOCOL_ENV, WINDOWS_WRAPPER_STAY_OUT_EXIT_CODE } from "./service/windows-wrapper-exit";
|
|
33
|
+
import { buildWindowsServiceScript, windowsTaskActionMatches } from "./service/windows-taskxml";
|
|
34
|
+
import { inspectServiceStateEvidence, serviceStatePathsForOpenCodexHome } from "./service/state";
|
|
31
35
|
|
|
32
36
|
/** Short: this runs inside admission, and a slow answer is the same as none. */
|
|
33
37
|
export const SERVICE_PROBE_TIMEOUT_MS = 2_000;
|
|
@@ -192,6 +196,8 @@ export interface ProbeDeps {
|
|
|
192
196
|
readonly windowsLocale?: string;
|
|
193
197
|
/** Startup-local full-listing cache; targeted task queries always bypass it. */
|
|
194
198
|
readonly windowsTaskListingCache?: WindowsTaskListingCache;
|
|
199
|
+
/** Service-state evidence paths; production derives them from the effective config home. */
|
|
200
|
+
readonly statePaths?: readonly string[];
|
|
195
201
|
}
|
|
196
202
|
|
|
197
203
|
const LABEL = "com.opencodex.proxy";
|
|
@@ -559,9 +565,95 @@ function decodeBatchPathValue(
|
|
|
559
565
|
.replaceAll(escapedPercent, "%");
|
|
560
566
|
}
|
|
561
567
|
|
|
562
|
-
/**
|
|
563
|
-
function
|
|
564
|
-
|
|
568
|
+
/** One generated quoted assignment, rejecting unquoted and duplicate forms. */
|
|
569
|
+
function generatedBatchSetValue(body: string, name: string): string | null {
|
|
570
|
+
const assignments = body.split(/\r?\n/).filter(line => new RegExp(`^\\s*@?set\\s+"?${name}=`, "i").test(line));
|
|
571
|
+
return assignments.length === 1 ? batchSetValue(assignments[0]!, name) : null;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/** Compare executable lines with the actual standalone generator's ordered script. */
|
|
575
|
+
function matchesGeneratedStandaloneControlFlow(body: string, port: number): boolean {
|
|
576
|
+
const scriptLines = (script: string): string[] => {
|
|
577
|
+
const lines = script.replace(/\r\n/g, "\n").split("\n");
|
|
578
|
+
if (lines.at(-1) === "") lines.pop();
|
|
579
|
+
return lines;
|
|
580
|
+
};
|
|
581
|
+
const lines = scriptLines(body);
|
|
582
|
+
const expected = scriptLines(buildWindowsServiceScript({
|
|
583
|
+
bun: "C:\\OpenCodex\\ocx.exe", bunRuntimeSource: "standalone", cli: null,
|
|
584
|
+
}, port, []));
|
|
585
|
+
const tokenBlock = 'if exist "%OCX_API_TOKEN_FILE%" (';
|
|
586
|
+
const boundary = lines.indexOf(tokenBlock);
|
|
587
|
+
const expectedBoundary = expected.indexOf(tokenBlock);
|
|
588
|
+
if (boundary < 0 || expectedBoundary < 0) return false;
|
|
589
|
+
const allowed = [
|
|
590
|
+
"OCX_SERVICE", WINDOWS_WRAPPER_PROTOCOL_ENV, BUN_RUNTIME_SOURCE_ENV,
|
|
591
|
+
BUN_RUNTIME_PATH_ENV, "PATH", "CODEX_HOME", "CODEX_SQLITE_HOME",
|
|
592
|
+
"OPENCODEX_HOME", "HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY", "NO_PROXY",
|
|
593
|
+
"OCX_API_TOKEN_FILE", "OCX_SERVICE_LOG", "OCX_BUN",
|
|
594
|
+
];
|
|
595
|
+
const required = [
|
|
596
|
+
"OCX_SERVICE", WINDOWS_WRAPPER_PROTOCOL_ENV, BUN_RUNTIME_SOURCE_ENV,
|
|
597
|
+
BUN_RUNTIME_PATH_ENV, "OCX_API_TOKEN_FILE",
|
|
598
|
+
"OCX_SERVICE_LOG", "OCX_BUN",
|
|
599
|
+
];
|
|
600
|
+
let previous = -1;
|
|
601
|
+
const seen = new Set<string>();
|
|
602
|
+
for (const line of lines.slice(0, boundary)) {
|
|
603
|
+
if (line === 'set "ERRORLEVEL="') continue;
|
|
604
|
+
if (!line.startsWith('set "')) continue;
|
|
605
|
+
const name = /^set "([A-Z_]+)=[^"\r\n]*"$/.exec(line)?.[1];
|
|
606
|
+
const index = name ? allowed.indexOf(name) : -1;
|
|
607
|
+
if (index <= previous || !name) return false;
|
|
608
|
+
previous = index;
|
|
609
|
+
seen.add(name);
|
|
610
|
+
}
|
|
611
|
+
if (required.some(name => !seen.has(name))) return false;
|
|
612
|
+
const withoutPrefixSets = (scriptLines: string[], end: number): string[] =>
|
|
613
|
+
scriptLines.filter((line, index) => index >= end || line === 'set "ERRORLEVEL="' || !line.startsWith('set "'));
|
|
614
|
+
const actualFlow = withoutPrefixSets(lines, boundary);
|
|
615
|
+
const generatedFlow = withoutPrefixSets(expected, expectedBoundary);
|
|
616
|
+
return actualFlow.length === generatedFlow.length
|
|
617
|
+
&& actualFlow.every((line, index) => line === generatedFlow[index]);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/** Validate the generated launch shape before interpreting omitted optional homes. */
|
|
621
|
+
function wrapperLaunchShape(body: string): "source" | { standaloneBun: string } | null {
|
|
622
|
+
if (!/^:loop\s*$/im.test(body)) return null;
|
|
623
|
+
const launchLines = body.split(/\r?\n/).filter(line => /^\s*"%OCX_BUN%"/i.test(line));
|
|
624
|
+
if (launchLines.length !== 1) return null;
|
|
625
|
+
const launch = launchLines[0]!.trim();
|
|
626
|
+
const sourceLaunch = /^"%OCX_BUN%" "%OCX_CLI%" start --port ([0-9]{1,5}) >>"%OCX_SERVICE_LOG%" 2>&1$/i;
|
|
627
|
+
const standaloneLaunch = /^"%OCX_BUN%" start --port ([0-9]{1,5}) >>"%OCX_SERVICE_LOG%" 2>&1$/i;
|
|
628
|
+
const bun = generatedBatchSetValue(body, "OCX_BUN");
|
|
629
|
+
if (!bun) return null;
|
|
630
|
+
const cliAssignments = body.split(/\r?\n/).filter(line => /^\s*@?set\s+"?OCX_CLI=/i.test(line));
|
|
631
|
+
if (cliAssignments.length > 0) {
|
|
632
|
+
const port = sourceLaunch.exec(launch)?.[1];
|
|
633
|
+
return cliAssignments.length === 1 && Boolean(generatedBatchSetValue(body, "OCX_CLI"))
|
|
634
|
+
&& Boolean(port) && Number(port) >= 1 && Number(port) <= 65535
|
|
635
|
+
? "source" : null;
|
|
636
|
+
}
|
|
637
|
+
const port = standaloneLaunch.exec(launch)?.[1];
|
|
638
|
+
if (!port || Number(port) < 1 || Number(port) > 65535 || !/^@echo off\s*$/im.test(body)
|
|
639
|
+
|| !/^setlocal EnableExtensions DisableDelayedExpansion\s*$/im.test(body)
|
|
640
|
+
|| !new RegExp(`^if "%ERRORLEVEL%"=="${WINDOWS_WRAPPER_STAY_OUT_EXIT_CODE}" goto stopped\\s*$`, "im").test(body)
|
|
641
|
+
|| !/^:stopped\s*$/im.test(body)) return null;
|
|
642
|
+
if (!matchesGeneratedStandaloneControlFlow(body, Number(port))) return null;
|
|
643
|
+
for (const [name, value] of [
|
|
644
|
+
["OCX_SERVICE", "1"],
|
|
645
|
+
[WINDOWS_WRAPPER_PROTOCOL_ENV, "1"],
|
|
646
|
+
[BUN_RUNTIME_SOURCE_ENV, "standalone"],
|
|
647
|
+
]) {
|
|
648
|
+
if (generatedBatchSetValue(body, name) !== value) return null;
|
|
649
|
+
}
|
|
650
|
+
const runtimePath = generatedBatchSetValue(body, BUN_RUNTIME_PATH_ENV);
|
|
651
|
+
if (!bun || !runtimePath || normalizeWindowsPath(decodeBatchPathValue(bun)) !== normalizeWindowsPath(decodeBatchPathValue(runtimePath))) return null;
|
|
652
|
+
for (const name of ["CODEX_HOME", "OPENCODEX_HOME"]) {
|
|
653
|
+
const assignments = body.split(/\r?\n/).filter(line => new RegExp(`^\\s*@?set\\s+"?${name}=`, "i").test(line));
|
|
654
|
+
if (assignments.length > 0 && generatedBatchSetValue(body, name) === null) return null;
|
|
655
|
+
}
|
|
656
|
+
return { standaloneBun: decodeBatchPathValue(bun) };
|
|
565
657
|
}
|
|
566
658
|
|
|
567
659
|
function normalizeWindowsPath(value: string): string {
|
|
@@ -711,7 +803,7 @@ function probeWinswRegistration(
|
|
|
711
803
|
|
|
712
804
|
function inspectWindows(
|
|
713
805
|
deps: Required<Pick<ProbeDeps, "runRaw" | "home">>
|
|
714
|
-
& Pick<ProbeDeps, "configDir" | "winswStatus" | "windowsLocale" | "windowsTaskListingCache">,
|
|
806
|
+
& Pick<ProbeDeps, "configDir" | "winswStatus" | "windowsLocale" | "windowsTaskListingCache" | "statePaths">,
|
|
715
807
|
): ServiceManagerInstallation {
|
|
716
808
|
const configDir = windowsConfigDirPath(deps);
|
|
717
809
|
const taskXmlPath = join(configDir, "opencodex-service-task.xml");
|
|
@@ -802,6 +894,10 @@ function inspectWindows(
|
|
|
802
894
|
const registeredWalk = walkWindowsChain(deps, registration.registeredXml, taskXmlPath);
|
|
803
895
|
if (registeredWalk.kind !== "present") return registeredWalk;
|
|
804
896
|
const registeredClaim = registeredWalk.claims[0];
|
|
897
|
+
const registeredLauncher = /"([^"]+)"/.exec(windowsTaskArguments(registration.registeredXml) ?? "")?.[1];
|
|
898
|
+
if (!registeredLauncher || !windowsTaskActionMatches(registration.registeredXml, registeredLauncher)) {
|
|
899
|
+
return unknown("the registered scheduled-task action does not match the generated launcher action");
|
|
900
|
+
}
|
|
805
901
|
if (!homesEqual(registeredClaim.homes, stagedClaim.homes)) {
|
|
806
902
|
return unknown("the registered scheduled task names different homes than the staged task definition");
|
|
807
903
|
}
|
|
@@ -837,7 +933,7 @@ function homesEqual(
|
|
|
837
933
|
* generated service-asset directory.
|
|
838
934
|
*/
|
|
839
935
|
function walkWindowsChain(
|
|
840
|
-
deps: Required<Pick<ProbeDeps, "home">> & Pick<ProbeDeps, "configDir" | "windowsLocale">,
|
|
936
|
+
deps: Required<Pick<ProbeDeps, "home">> & Pick<ProbeDeps, "configDir" | "windowsLocale" | "statePaths">,
|
|
841
937
|
xml: string,
|
|
842
938
|
definitionPath: string,
|
|
843
939
|
): ServiceManagerInstallation {
|
|
@@ -885,9 +981,21 @@ function walkWindowsChain(
|
|
|
885
981
|
return unknown(`the launcher wrapper could not be read: ${String(error)}`);
|
|
886
982
|
}
|
|
887
983
|
|
|
888
|
-
|
|
984
|
+
const launchShape = wrapperLaunchShape(wrapperBody);
|
|
985
|
+
if (launchShape === null) {
|
|
889
986
|
return unknown(`the launcher wrapper does not look like a generated opencodex service wrapper: ${wrapperPath}`);
|
|
890
987
|
}
|
|
988
|
+
if (typeof launchShape !== "string") {
|
|
989
|
+
const executable = launchShape.standaloneBun;
|
|
990
|
+
const evidence = inspectServiceStateEvidence(deps.statePaths ?? serviceStatePathsForOpenCodexHome(configDir));
|
|
991
|
+
const valid = evidence.filter(e => e.kind === "valid");
|
|
992
|
+
if (!win32Path.isAbsolute(executable) || win32Path.extname(executable).toLowerCase() !== ".exe"
|
|
993
|
+
|| valid.length === 0 || evidence.some(e => e.kind === "invalid" || e.kind === "unreadable")
|
|
994
|
+
|| valid.some(e => e.state.version !== 2 || e.state.backend !== "scheduler" || e.state.cliPath !== null
|
|
995
|
+
|| !e.state.bunPath || normalizeWindowsPath(e.state.bunPath) !== normalizeWindowsPath(executable))) {
|
|
996
|
+
return unknown("the standalone service wrapper executable is not bound to recorded scheduler install state");
|
|
997
|
+
}
|
|
998
|
+
}
|
|
891
999
|
|
|
892
1000
|
const rawCodexHome = batchSetValue(wrapperBody, "CODEX_HOME");
|
|
893
1001
|
const rawOpencodexHome = batchSetValue(wrapperBody, "OPENCODEX_HOME");
|
|
@@ -992,6 +1100,7 @@ export function inspectServiceManagerInstallation(deps: ProbeDeps = {}): Service
|
|
|
992
1100
|
winswStatus: deps.winswStatus,
|
|
993
1101
|
windowsLocale: deps.windowsLocale,
|
|
994
1102
|
windowsTaskListingCache: deps.windowsTaskListingCache,
|
|
1103
|
+
statePaths: deps.statePaths,
|
|
995
1104
|
});
|
|
996
1105
|
}
|
|
997
1106
|
return unknown(`no service manager probe for platform ${platform}`);
|
package/src/types/wire.ts
CHANGED
|
@@ -87,6 +87,13 @@ const WIRE_ADAPTER_PIN_PREFIXES: Readonly<Record<string, WirePinPrefixRule>> = O
|
|
|
87
87
|
endpoint: "https://api.commandcode.ai/provider/v1",
|
|
88
88
|
prefixes: Object.freeze({ "claude-": "anthropic" }),
|
|
89
89
|
}),
|
|
90
|
+
// TokenLab declares Claude ids as Chat + Anthropic Messages, never Responses
|
|
91
|
+
// (`tokenlab.accepted_request_formats` on GET /v1/models/{id}, all 11 live claude-* ids on
|
|
92
|
+
// 2026-09-30). Messages is their native wire; POST /v1/messages accepts x-api-key.
|
|
93
|
+
tokenlab: Object.freeze({
|
|
94
|
+
endpoint: "https://api.tokenlab.sh/v1",
|
|
95
|
+
prefixes: Object.freeze({ "claude-": "anthropic" }),
|
|
96
|
+
}),
|
|
90
97
|
});
|
|
91
98
|
|
|
92
99
|
/** Just enough of a provider config to tell whether it still points at the pinned endpoint. */
|
|
@@ -51,6 +51,12 @@ const ASTRA_API_PRICING = "https://developers.openai.com/api/docs/models/gpt-6-a
|
|
|
51
51
|
const GPT6_SOL: Cost4 = { input: 2, output: 10, cacheRead: 0.2, cacheWrite: 2.5 };
|
|
52
52
|
const GPT6_LUNA: Cost4 = { input: 0.1, output: 0.5, cacheRead: 0.01, cacheWrite: 0.125 };
|
|
53
53
|
const GPT6_API_PRICING = "https://developers.openai.com/api/docs/changelog (2026-09-22: GPT-6 Sol $2 / $0.20 cached / $10; GPT-6 Luna $0.10 / $0.01 cached / $0.50)";
|
|
54
|
+
/**
|
|
55
|
+
* GPT-6.1 Sol API list prices (released 2026-09-29). Same input, output and cache write as GPT-6
|
|
56
|
+
* Sol; only cached input halves, to $0.10. Prompts over 272K take the OPENAI_LONG_CONTEXT tier.
|
|
57
|
+
*/
|
|
58
|
+
const GPT61_SOL: Cost4 = { input: 2, output: 10, cacheRead: 0.1, cacheWrite: 2.5 };
|
|
59
|
+
const GPT61_SOL_API_PRICING = "https://developers.openai.com/api/docs/pricing (2026-09-30: GPT-6.1 Sol $2 / $0.10 cached / $2.50 cache write / $10; >272K $4 / $0.20 / $5 / $15)";
|
|
54
60
|
/**
|
|
55
61
|
* Daybreak aliases. `daybreak-*-latest` never appears in the pricing table itself — only its
|
|
56
62
|
* current snapshot does — so these tuples are the snapshot's published rates and carry
|
|
@@ -256,8 +262,10 @@ export const EXPECTED_PRICE_OVERLAYS: readonly ExpectedPriceOverlay[] = [
|
|
|
256
262
|
{ provider: "openai-apikey", modelId: "gpt-6-astra", cost4: GPT6_ASTRA, source: ASTRA_API_PRICING, verifiedAt: "2026-09-05", status: "verified" },
|
|
257
263
|
// Display estimates use API prices for both login and API-key routes, including cache writes.
|
|
258
264
|
{ provider: "openai", modelId: "gpt-6-astra", cost4: GPT6_ASTRA, source: `API-reference comparison estimate: ${ASTRA_API_PRICING}`, verifiedAt: "2026-09-05", status: "verified-derived" },
|
|
265
|
+
{ provider: "openai-apikey", modelId: "gpt-6.1-sol", cost4: GPT61_SOL, source: GPT61_SOL_API_PRICING, verifiedAt: "2026-09-30", status: "verified" },
|
|
259
266
|
{ provider: "openai-apikey", modelId: "gpt-6-sol", cost4: GPT6_SOL, source: GPT6_API_PRICING, verifiedAt: "2026-09-23", status: "verified" },
|
|
260
267
|
{ provider: "openai-apikey", modelId: "gpt-6-luna", cost4: GPT6_LUNA, source: GPT6_API_PRICING, verifiedAt: "2026-09-23", status: "verified" },
|
|
268
|
+
{ provider: "openai", modelId: "gpt-6.1-sol", cost4: GPT61_SOL, source: `API-reference comparison estimate: ${GPT61_SOL_API_PRICING}`, verifiedAt: "2026-09-30", status: "verified-derived" },
|
|
261
269
|
{ provider: "openai", modelId: "gpt-6-sol", cost4: GPT6_SOL, source: `API-reference comparison estimate: ${GPT6_API_PRICING}`, verifiedAt: "2026-09-23", status: "verified-derived" },
|
|
262
270
|
{ provider: "openai", modelId: "gpt-6-luna", cost4: GPT6_LUNA, source: `API-reference comparison estimate: ${GPT6_API_PRICING}`, verifiedAt: "2026-09-23", status: "verified-derived" },
|
|
263
271
|
// claude-fable-5-1 now HAS a generated jawcode row, so the two Anthropic surfaces resolve
|
|
@@ -459,6 +467,7 @@ export const EXPECTED_PRICE_OVERLAYS: readonly ExpectedPriceOverlay[] = [
|
|
|
459
467
|
{ provider: "devin-cli", modelId: "swe-1-6", cost4: DEVIN_SWE_17, source: DEVIN_PRICING, verifiedAt: "2026-09-13", status: "verified-derived" },
|
|
460
468
|
{ provider: "devin-cli", modelId: "gpt-5-6-sol", cost4: GPT56_SOL, source: `enterprise list column (self-serve shows discounted 1.2/6); ${DEVIN_PRICING}`, verifiedAt: "2026-09-13", status: "verified-derived" },
|
|
461
469
|
{ provider: "devin-cli", modelId: "gpt-6-astra", cost4: GPT6_ASTRA, source: DEVIN_PRICING, verifiedAt: "2026-09-13", status: "verified-derived" },
|
|
470
|
+
{ provider: "devin-cli", modelId: "gpt-6-1-sol", cost4: GPT61_SOL, source: `derived: GPT-6.1 Sol added 2026-09-30 ahead of Devin's modelCostData table (devin.ai/blog/gpt-6-1-sol; uid spelled like gpt-5-6-sol); OpenAI API list price ${GPT61_SOL_API_PRICING}`, verifiedAt: "2026-09-30", status: "verified-derived" },
|
|
462
471
|
{ provider: "devin-cli", modelId: "gpt-6-sol", cost4: GPT6_SOL, source: `derived: GPT-6 Sol/Luna added 2026-09-23 ahead of Devin's modelCostData table; OpenAI API list price ${GPT6_API_PRICING}`, verifiedAt: "2026-09-23", status: "verified-derived" },
|
|
463
472
|
{ provider: "devin-cli", modelId: "gpt-6-luna", cost4: GPT6_LUNA, source: `derived: GPT-6 Sol/Luna added 2026-09-23 ahead of Devin's modelCostData table; OpenAI API list price ${GPT6_API_PRICING}`, verifiedAt: "2026-09-23", status: "verified-derived" },
|
|
464
473
|
{ provider: "devin-cli", modelId: "claude-opus-5", cost4: CLAUDE_OPUS_46, source: DEVIN_PRICING, verifiedAt: "2026-09-13", status: "verified-derived" },
|
|
@@ -477,6 +486,7 @@ export const EXPECTED_PRICE_OVERLAYS: readonly ExpectedPriceOverlay[] = [
|
|
|
477
486
|
{ provider: "devin", modelId: "gpt-5-6-sol", cost4: GPT56_SOL, source: `enterprise list column (self-serve shows discounted 1.2/6); ${DEVIN_PRICING}`, verifiedAt: "2026-09-13", status: "verified-derived" },
|
|
478
487
|
{ provider: "devin", modelId: "gpt-5-6-luna", cost4: GPT56_LUNA, source: DEVIN_PRICING, verifiedAt: "2026-09-13", status: "verified-derived" },
|
|
479
488
|
{ provider: "devin", modelId: "gpt-5-6-terra", cost4: GPT56_TERRA, source: DEVIN_PRICING, verifiedAt: "2026-09-13", status: "verified-derived" },
|
|
489
|
+
{ provider: "devin", modelId: "gpt-6-1-sol", cost4: GPT61_SOL, source: `derived: GPT-6.1 Sol added 2026-09-30 ahead of Devin's modelCostData table (devin.ai/blog/gpt-6-1-sol; uid spelled like gpt-5-6-sol); OpenAI API list price ${GPT61_SOL_API_PRICING}`, verifiedAt: "2026-09-30", status: "verified-derived" },
|
|
480
490
|
{ provider: "devin", modelId: "gpt-6-sol", cost4: GPT6_SOL, source: `derived: GPT-6 Sol/Luna added 2026-09-23 ahead of Devin's modelCostData table; OpenAI API list price ${GPT6_API_PRICING}`, verifiedAt: "2026-09-23", status: "verified-derived" },
|
|
481
491
|
{ provider: "devin", modelId: "gpt-6-luna", cost4: GPT6_LUNA, source: `derived: GPT-6 Sol/Luna added 2026-09-23 ahead of Devin's modelCostData table; OpenAI API list price ${GPT6_API_PRICING}`, verifiedAt: "2026-09-23", status: "verified-derived" },
|
|
482
492
|
{ provider: "devin", modelId: "claude-opus-4-8", cost4: CLAUDE_OPUS_46, source: DEVIN_PRICING, verifiedAt: "2026-09-13", status: "verified-derived" },
|
|
@@ -690,6 +700,7 @@ const UNIFORM_DOUBLE: Cost4 = { input: 2, output: 2, cacheRead: 2, cacheWrite: 2
|
|
|
690
700
|
const OPENAI_PRICING_DOC = "https://developers.openai.com/api/docs/pricing";
|
|
691
701
|
const OPENAI_CONTEXT_MODELS = [
|
|
692
702
|
"gpt-6-astra",
|
|
703
|
+
"gpt-6.1-sol",
|
|
693
704
|
"gpt-6-sol",
|
|
694
705
|
"gpt-6-luna",
|
|
695
706
|
"gpt-5.6-sol",
|