@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.
Files changed (79) hide show
  1. package/gui/dist/assets/{App-Cf-WYstE.js → App-mHGa7dE1.js} +15 -15
  2. package/gui/dist/assets/{Tray-DkM0mULy.js → Tray-BxY_RYZg.js} +1 -1
  3. package/gui/dist/assets/{index-PWrwOfio.js → index-ur_BPgK1.js} +12 -12
  4. package/gui/dist/assets/{quota-summary-D-FKtrgr.js → quota-summary-D1Vq7N9H.js} +1 -1
  5. package/gui/dist/index.html +1 -1
  6. package/package.json +1 -1
  7. package/src/adapters/cursor/native-exec.ts +28 -10
  8. package/src/adapters/devin/live-models.ts +5 -0
  9. package/src/adapters/kiro/reasoning.ts +2 -0
  10. package/src/adapters/openai-chat/messages.ts +4 -1
  11. package/src/adapters/openai-responses/passthrough.ts +12 -1
  12. package/src/adapters/openai-responses/reasoning.ts +15 -0
  13. package/src/bridge/internal.ts +12 -0
  14. package/src/cli/account-anthropic-threshold.ts +41 -0
  15. package/src/cli/account-api.ts +2 -0
  16. package/src/cli/account-extended.ts +32 -21
  17. package/src/cli/account.ts +1 -0
  18. package/src/cli/capabilities.ts +8 -4
  19. package/src/cli/index.ts +1 -1
  20. package/src/codex/catalog/build-entries.ts +6 -5
  21. package/src/codex/catalog/derive-entry.ts +3 -4
  22. package/src/codex/catalog/gather-capture.ts +2 -0
  23. package/src/codex/catalog/metadata.ts +6 -0
  24. package/src/codex/catalog/native-models.ts +17 -3
  25. package/src/codex/catalog/parsing.ts +4 -1
  26. package/src/codex/catalog/pinned-models.ts +3 -1
  27. package/src/codex/catalog/provider-models.ts +22 -2
  28. package/src/codex/data/roster-pinned-models.json +178 -1
  29. package/src/codex/model-entitlements.ts +1 -1
  30. package/src/codex/native-profile-store.ts +2 -1
  31. package/src/combos/resolve.ts +16 -1
  32. package/src/config/subagent-models.ts +30 -5
  33. package/src/generated/compatibility-version.json +95 -67
  34. package/src/generated/model-metadata.ts +4 -4
  35. package/src/lib/account-selection-events.ts +28 -0
  36. package/src/lib/errors.ts +7 -5
  37. package/src/lib/keyring-native.ts +132 -0
  38. package/src/lib/provider-outbound.ts +13 -0
  39. package/src/lib/retry-delay.ts +10 -0
  40. package/src/oauth/anthropic-account-threshold.ts +13 -0
  41. package/src/oauth/anthropic-routing.ts +223 -52
  42. package/src/oauth/index.ts +4 -2
  43. package/src/oauth/store.ts +45 -3
  44. package/src/oauth/types.ts +2 -0
  45. package/src/providers/api-key-resolve.ts +2 -4
  46. package/src/providers/codebuddy-models.ts +6 -0
  47. package/src/providers/kiro-models.ts +4 -0
  48. package/src/providers/label.ts +20 -1
  49. package/src/providers/quota/account-cache.ts +2 -1
  50. package/src/providers/quota/anthropic-cooldown-recovery.ts +118 -0
  51. package/src/providers/quota/vendor-probes-oauth.ts +84 -57
  52. package/src/providers/quota.ts +45 -46
  53. package/src/providers/registry/entries-core.ts +3 -1
  54. package/src/providers/registry/entries-extended.ts +14 -1
  55. package/src/providers/registry/model-seeds.ts +7 -1
  56. package/src/server/claude-messages.ts +84 -27
  57. package/src/server/management/anthropic-account-threshold.ts +27 -0
  58. package/src/server/management/oauth-account-routes.ts +17 -4
  59. package/src/server/management/route-registry.ts +1 -0
  60. package/src/server/messages-native-oauth.ts +20 -21
  61. package/src/server/messages-native.ts +39 -14
  62. package/src/server/relay-eager.ts +5 -2
  63. package/src/server/relay.ts +268 -82
  64. package/src/server/responses/adapter-continuation.ts +6 -0
  65. package/src/server/responses/adapter-delivery.ts +25 -7
  66. package/src/server/responses/adapter-dispatch.ts +31 -0
  67. package/src/server/responses/buffered-sse-json.ts +345 -0
  68. package/src/server/responses/core-codex-account.ts +4 -1
  69. package/src/server/responses/core-combo.ts +28 -0
  70. package/src/server/responses/core-lifetime.ts +4 -0
  71. package/src/server/responses/passthrough-delivery.ts +305 -10
  72. package/src/server/responses/passthrough-dispatch.ts +60 -25
  73. package/src/server/responses/passthrough-error.ts +3 -1
  74. package/src/server/responses/request-transport.ts +22 -5
  75. package/src/server/responses/terminal-error-redaction.ts +53 -0
  76. package/src/service/windows-taskxml.ts +20 -6
  77. package/src/service-manager-probe.ts +115 -6
  78. package/src/types/wire.ts +7 -0
  79. 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
- const revisedAnthropicId = route.providerName === "anthropic" && anthropicRouteDecision
195
- ? resolveAnthropicAccountForSession(anthropicSessionKey, config, Date.now(), anthropicRouteDecision).accountId : null;
196
- if (route.providerName === "anthropic" && anthropicRouteDecision && !revisedAnthropicId) return null;
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
- if (!servingOAuthSnapshot || !await applyFailoverSnapshot(servingOAuthSnapshot, requestParsed)) {
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
- /** Validate the generated wrapper before interpreting omitted optional homes. */
563
- function wrapperLooksGenerated(body: string): boolean {
564
- return /:loop\s*[\s\S]*^"%OCX_BUN%" "%OCX_CLI%" start\b[^\r\n]*$/im.test(body);
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
- if (!wrapperLooksGenerated(wrapperBody)) {
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",