@bitkyc08/opencodex 2.90.0 → 2.92.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (250) hide show
  1. package/bin/ocx.mjs +37 -3
  2. package/gui/dist/assets/App-DMps4OLk.css +1 -0
  3. package/gui/dist/assets/{App-DgxkE6Md.js → App-sUf_8afJ.js} +18 -18
  4. package/gui/dist/assets/{Tray-DbTNc_88.js → Tray-BSsxznDm.js} +1 -1
  5. package/gui/dist/assets/index-BlFE_VB_.js +94 -0
  6. package/gui/dist/assets/quota-summary-BRlbXgw0.js +1 -0
  7. package/gui/dist/index.html +1 -1
  8. package/gui/dist/provider-icons/workbuddy-color.svg +29 -0
  9. package/package.json +4 -3
  10. package/src/adapters/anthropic.ts +2 -5
  11. package/src/adapters/claude-cli/usage-admission.ts +14 -0
  12. package/src/adapters/google-vertex-endpoint.ts +9 -1
  13. package/src/adapters/google-vertex-key-project.ts +223 -0
  14. package/src/adapters/google.ts +13 -3
  15. package/src/adapters/openai-chat/tool-name-registry.ts +3 -1
  16. package/src/adapters/openai-chat.ts +2 -2
  17. package/src/adapters/openai-responses/passthrough.ts +2 -1
  18. package/src/adapters/openai-responses/request-strips.ts +92 -0
  19. package/src/adapters/openai-responses/transport.ts +6 -0
  20. package/src/adapters/registry.ts +5 -0
  21. package/src/adapters/workbuddy-schema.ts +61 -0
  22. package/src/adapters/workbuddy.ts +68 -0
  23. package/src/bridge/response-json.ts +1 -1
  24. package/src/bridge/sse.ts +1 -1
  25. package/src/chat/inbound.ts +20 -2
  26. package/src/chat/outbound.ts +47 -8
  27. package/src/chat/reasoning-details.ts +137 -0
  28. package/src/cli/capabilities-agents-routing.ts +53 -3
  29. package/src/cli/capabilities-base.ts +15 -2
  30. package/src/cli/capabilities-observe-system.ts +4 -4
  31. package/src/cli/codex-cli-update.ts +8 -0
  32. package/src/cli/codex-shim-autorestore.ts +5 -0
  33. package/src/cli/combo-result.ts +6 -0
  34. package/src/cli/combo-stats.ts +3 -0
  35. package/src/cli/combo.ts +66 -42
  36. package/src/cli/dispatch.ts +30 -0
  37. package/src/cli/fallback.ts +127 -0
  38. package/src/cli/help.ts +1 -0
  39. package/src/cli/index.ts +1 -2
  40. package/src/cli/launcher-context.ts +49 -2
  41. package/src/cli/link-apply-restart.ts +72 -0
  42. package/src/cli/link-child.ts +6 -2
  43. package/src/cli/link-cleanup.ts +95 -0
  44. package/src/cli/link-enrollment.ts +4 -0
  45. package/src/cli/link.ts +123 -35
  46. package/src/cli/provider.ts +46 -3
  47. package/src/cli/registry.ts +13 -1
  48. package/src/cli/restart-failure.ts +4 -0
  49. package/src/cli/runtime-api.ts +1 -1
  50. package/src/cli/status-codex-settings.ts +56 -0
  51. package/src/cli/status-fallback.ts +29 -0
  52. package/src/cli/status-install.ts +52 -0
  53. package/src/cli/status.ts +20 -1
  54. package/src/cli/system-restart-client.ts +32 -1
  55. package/src/cli/update-restart-child.ts +4 -3
  56. package/src/cli/update-restart-home.ts +20 -8
  57. package/src/cli/usage-compare.ts +68 -12
  58. package/src/client/connect.ts +82 -6
  59. package/src/client/home-unreachable.ts +16 -0
  60. package/src/client/hub-relay.ts +8 -2
  61. package/src/client/link-home.ts +119 -6
  62. package/src/client/link-ingress.ts +1 -1
  63. package/src/client/link-parity-reporter.ts +107 -0
  64. package/src/client/link-readiness.ts +97 -0
  65. package/src/client/link-relay-transport.ts +84 -9
  66. package/src/client/link-relay.ts +98 -15
  67. package/src/client/link-tunnel.ts +45 -18
  68. package/src/client/machine-listener.ts +103 -3
  69. package/src/client/runtime-restart.ts +127 -0
  70. package/src/client/runtime.ts +155 -25
  71. package/src/codex/account-store.ts +17 -3
  72. package/src/codex/app-server-restart-service.ts +6 -5
  73. package/src/codex/auth-context.ts +10 -6
  74. package/src/codex/catalog/effort.ts +9 -2
  75. package/src/codex/catalog/gather-capture.ts +4 -2
  76. package/src/codex/catalog/parsing.ts +4 -0
  77. package/src/codex/catalog/provider-models.ts +14 -8
  78. package/src/codex/chatgpt-refresh-failure.ts +6 -2
  79. package/src/codex/cli-install-provenance.ts +418 -0
  80. package/src/codex/cli-installation-targets.ts +5 -1
  81. package/src/codex/cli-update-plan.ts +43 -16
  82. package/src/codex/desktop-app/picker-freshness.ts +10 -8
  83. package/src/codex/desktop-compatibility/usage-activation.ts +2 -1
  84. package/src/codex/desktop-compatibility/usage-controller.ts +7 -1
  85. package/src/codex/desktop-compatibility/windows-key-protection.ts +18 -6
  86. package/src/codex/pool-refresh-backoff.ts +3 -1
  87. package/src/codex/quota-history.ts +7 -0
  88. package/src/codex/quota.ts +10 -0
  89. package/src/codex/subagent-model-fallback.ts +24 -1
  90. package/src/codex/user-identity.ts +35 -6
  91. package/src/codex/windows-current-sid.ts +162 -0
  92. package/src/combos/index.ts +1 -0
  93. package/src/combos/jev-quota-activation.ts +5 -1
  94. package/src/combos/jev-quota-route.ts +1 -1
  95. package/src/combos/jev-quota.ts +9 -1
  96. package/src/combos/jev-session-pin-config.ts +51 -0
  97. package/src/combos/jev.ts +6 -0
  98. package/src/combos/resolve.ts +12 -0
  99. package/src/combos/types.ts +19 -0
  100. package/src/config/diagnostics.ts +5 -0
  101. package/src/config/load-degrade.ts +2 -0
  102. package/src/config/provider-validation.ts +7 -0
  103. package/src/config/schema/config-schema.ts +3 -0
  104. package/src/config/schema/fallback-policy.ts +40 -0
  105. package/src/config/schema/leaf-validators.ts +1 -0
  106. package/src/generated/compatibility-version.json +375 -195
  107. package/src/lib/authorization-scheme.ts +95 -0
  108. package/src/lib/decision-quota-sink.ts +10 -0
  109. package/src/lib/provider-client-headers.ts +1 -0
  110. package/src/lib/redact.ts +2 -2
  111. package/src/lib/system-restart-contract.ts +24 -0
  112. package/src/lib/test-home-guard.ts +78 -11
  113. package/src/lib/translator-budget.ts +4 -4
  114. package/src/lib/upstream-retry.ts +4 -1
  115. package/src/link/child-parity.ts +124 -0
  116. package/src/link/codex-settings.ts +37 -0
  117. package/src/link/compatibility.ts +119 -0
  118. package/src/link/diagnostics.ts +193 -0
  119. package/src/link/management-routes.ts +2 -0
  120. package/src/link/peer-report.ts +100 -0
  121. package/src/link/relay-auth.ts +6 -0
  122. package/src/link/remote-platform.ts +78 -0
  123. package/src/link/ssh-argv.ts +19 -0
  124. package/src/link/status-projection.ts +63 -2
  125. package/src/link/supervisor.ts +45 -12
  126. package/src/link/tunnel-history.ts +109 -0
  127. package/src/link/tunnel-state.ts +80 -17
  128. package/src/messaging/remote-files.ts +24 -1
  129. package/src/oauth/generic-account-failover.ts +91 -7
  130. package/src/oauth/index.ts +16 -1
  131. package/src/oauth/kimi.ts +2 -0
  132. package/src/oauth/model-discovery-auth.ts +5 -0
  133. package/src/oauth/nous.ts +4 -1
  134. package/src/oauth/store.ts +6 -2
  135. package/src/oauth/token-guardian.ts +7 -0
  136. package/src/oauth/types.ts +9 -0
  137. package/src/oauth/workbuddy.ts +139 -0
  138. package/src/protocols/codecs/chat.ts +3 -3
  139. package/src/protocols/encoders/adapter-events.ts +26 -2
  140. package/src/protocols/encoders/chat.ts +55 -3
  141. package/src/providers/antigravity-quota-identity.ts +37 -0
  142. package/src/providers/base-url-choices.ts +8 -5
  143. package/src/providers/derive.ts +23 -3
  144. package/src/providers/mistral-transport.ts +37 -4
  145. package/src/providers/model-discovery.ts +2 -0
  146. package/src/providers/model-rename-fields.ts +1 -0
  147. package/src/providers/quota/account-cache.ts +3 -2
  148. package/src/providers/quota/antigravity.ts +27 -22
  149. package/src/providers/quota/report-cache.ts +13 -4
  150. package/src/providers/quota/vendor-probes-key.ts +60 -0
  151. package/src/providers/quota/workbuddy.ts +83 -0
  152. package/src/providers/quota-decision-antigravity.ts +64 -0
  153. package/src/providers/quota-decision-publication.ts +11 -0
  154. package/src/providers/quota-decision-snapshot.ts +21 -4
  155. package/src/providers/quota-types.ts +2 -0
  156. package/src/providers/quota.ts +3 -1
  157. package/src/providers/registry/entries-core.ts +9 -0
  158. package/src/providers/registry/entries-extended.ts +20 -7
  159. package/src/providers/registry/model-ids.ts +3 -0
  160. package/src/providers/registry/model-seeds.ts +27 -28
  161. package/src/providers/registry/types.ts +19 -1
  162. package/src/providers/resolved-model-policy.ts +4 -4
  163. package/src/providers/routed-config.ts +15 -0
  164. package/src/providers/stale-model-roster-migration.ts +83 -15
  165. package/src/providers/workbuddy-credit-balance.ts +81 -0
  166. package/src/providers/workbuddy-models.ts +37 -0
  167. package/src/providers/workbuddy.ts +60 -0
  168. package/src/quota-pool/assemble.ts +326 -0
  169. package/src/quota-pool/contract.ts +58 -0
  170. package/src/quota-pool/identity.ts +203 -0
  171. package/src/remote/features.ts +74 -0
  172. package/src/remote/protocol.ts +2 -0
  173. package/src/remote/served-by.ts +57 -0
  174. package/src/responses/parser.ts +7 -0
  175. package/src/responses/reasoning-envelope.ts +12 -0
  176. package/src/responses/reasoning-replay-cache.ts +18 -14
  177. package/src/router.ts +2 -0
  178. package/src/routing/analytics.ts +2 -3
  179. package/src/routing/fallback-policy.ts +290 -0
  180. package/src/routing/fallback-state.ts +179 -0
  181. package/src/server/audio-client.ts +2 -1
  182. package/src/server/audio-dictation.ts +2 -1
  183. package/src/server/audio-upstream.ts +2 -1
  184. package/src/server/auth-cors.ts +7 -5
  185. package/src/server/background-lifecycle.ts +4 -0
  186. package/src/server/chat-completions.ts +22 -2
  187. package/src/server/chat-native-reasoning.ts +10 -3
  188. package/src/server/chat-native.ts +13 -2
  189. package/src/server/claude-messages.ts +3 -0
  190. package/src/server/gui-session.ts +2 -1
  191. package/src/server/index/link-listener.ts +11 -1
  192. package/src/server/index/link-management.ts +47 -0
  193. package/src/server/index/link-relay-sessions.ts +20 -7
  194. package/src/server/index/optional-listeners.ts +11 -2
  195. package/src/server/index/serve-options.ts +19 -2
  196. package/src/server/live.ts +5 -0
  197. package/src/server/local-management-read-client.ts +3 -1
  198. package/src/server/management/combo-routes.ts +10 -4
  199. package/src/server/management/context.ts +1 -1
  200. package/src/server/management/fallback-routes.ts +75 -0
  201. package/src/server/management/link-enrollment.ts +21 -3
  202. package/src/server/management/link-route-state.ts +2 -0
  203. package/src/server/management/link-routes.ts +16 -2
  204. package/src/server/management/provider-routes.ts +1 -1
  205. package/src/server/management/quota-pool-routes.ts +24 -0
  206. package/src/server/management/route-registry.ts +3 -0
  207. package/src/server/management-api.ts +16 -0
  208. package/src/server/management-auth.ts +2 -1
  209. package/src/server/port-reclaim.ts +51 -9
  210. package/src/server/request-log-conversation.ts +49 -0
  211. package/src/server/request-log.ts +2 -0
  212. package/src/server/responses/agent-task-recovery.ts +1 -1
  213. package/src/server/responses/anthropic-thinking-recovery.ts +1 -0
  214. package/src/server/responses/compact.ts +12 -4
  215. package/src/server/responses/core-auth.ts +11 -1
  216. package/src/server/responses/core-combo.ts +125 -32
  217. package/src/server/responses/core-lifetime.ts +2 -2
  218. package/src/server/responses/core-normalize.ts +6 -3
  219. package/src/server/responses/core-opaque-recovery.ts +1 -0
  220. package/src/server/responses/core-options.ts +5 -0
  221. package/src/server/responses/core-replay.ts +2 -1
  222. package/src/server/responses/encrypted-payload.ts +3 -3
  223. package/src/server/responses/jev-combo-choice.ts +39 -0
  224. package/src/server/responses/jev-session-pin.ts +203 -0
  225. package/src/server/responses/main-hard-lock-routing.ts +54 -2
  226. package/src/server/responses/passthrough-delivery.ts +4 -0
  227. package/src/server/responses/policy-fallback.ts +44 -1
  228. package/src/server/responses/request-prepare.ts +19 -6
  229. package/src/server/responses/request-transport.ts +25 -6
  230. package/src/server/responses/run-turn-execution.ts +2 -4
  231. package/src/server/responses/sidecar-execution.ts +2 -4
  232. package/src/server/responses-phase-inference.ts +267 -0
  233. package/src/server/restart-replacement.ts +50 -2
  234. package/src/server/sse-payload-rewrite.ts +14 -1
  235. package/src/server/ws-bridge.ts +5 -0
  236. package/src/service/cli.ts +25 -2
  237. package/src/service/definition.ts +20 -8
  238. package/src/service/diagnostics.ts +20 -6
  239. package/src/service/status-json.ts +116 -0
  240. package/src/types/config.ts +13 -0
  241. package/src/types/fallback.ts +38 -0
  242. package/src/types/provider.ts +9 -1
  243. package/src/types/request.ts +1 -0
  244. package/src/types/wire.ts +1 -0
  245. package/src/update/codex-cli-update-launch-policy.d.mts +9 -0
  246. package/src/update/codex-cli-update-launch-policy.mjs +30 -0
  247. package/src/usage/jev-stats.ts +54 -14
  248. package/gui/dist/assets/App-dnkJ85te.css +0 -1
  249. package/gui/dist/assets/index-TmaHa54u.js +0 -94
  250. package/gui/dist/assets/quota-summary-B16x5DZZ.js +0 -1
@@ -50,6 +50,8 @@ import { validateDevinApiBaseUrl } from "./devin/api-base";
50
50
  import { loginGithubCopilot, refreshGithubCopilotToken, validateCopilotApiBaseUrl } from "./github-copilot";
51
51
  import { loginCommandCode, refreshCommandCodeToken } from "./command-code";
52
52
  import { loginMetaMuse, refreshMetaMuseToken } from "./meta-muse";
53
+ import { loginWorkBuddy, refreshWorkBuddyToken } from "./workbuddy";
54
+ import { WORKBUDDY_CONFIG_URL, workBuddyHeaders } from "../providers/workbuddy";
53
55
  import { DSH_ACCOUNT_AUTH } from "./dsh";
54
56
  import { loginOrcaRouter, orcaRouterInferenceBaseUrl, refreshOrcaRouterKey } from "./orcarouter";
55
57
  import { ANTIGRAVITY_DISCOVERY_UA } from "../adapters/google-antigravity-wire";
@@ -98,6 +100,7 @@ import { normalizeOAuthPlan, mergeOAuthPlan } from "./plan";
98
100
  import { mergeAnthropicIdentity, type AnthropicIdentityResolver } from "./anthropic-identity";
99
101
  import { AnthropicCrossInstanceDuplicateError, AnthropicLocalCliImportError, assertAnthropicCredentialSource } from "./store-anthropic-instance";
100
102
  export interface OAuthAccessSnapshot {
103
+ workbuddy?: import("./types").WorkBuddyOAuthMetadata;
101
104
  provider: string;
102
105
  accountId: string;
103
106
  generation: string;
@@ -246,6 +249,11 @@ function oauthDefaultModel(id: string): string {
246
249
  }
247
250
 
248
251
  export const OAUTH_PROVIDERS: Record<string, OAuthProviderDef> = {
252
+ "workbuddy-ai": {
253
+ login: loginWorkBuddy, refresh: refreshWorkBuddyToken,
254
+ providerConfig: oauthConfig("workbuddy-ai"), defaultModel: oauthDefaultModel("workbuddy-ai"),
255
+ defaultRefreshPolicy: "lazy-only",
256
+ },
249
257
  "dsh-account": {
250
258
  ...DSH_ACCOUNT_AUTH,
251
259
  providerConfig: oauthConfig("dsh-account"),
@@ -561,6 +569,7 @@ function accessSnapshot(provider: string, accountId: string, cred: OAuthCredenti
561
569
  accountId,
562
570
  generation: credentialGeneration(cred),
563
571
  accessToken: cred.access,
572
+ ...(oauthProvider === "workbuddy-ai" && cred.workbuddy ? { workbuddy: { ...cred.workbuddy } } : {}),
564
573
  ...(cred.projectId ? { projectId: cred.projectId } : {}),
565
574
  ...(accountApiBaseUrl ? { apiBaseUrl: accountApiBaseUrl } : {}),
566
575
  ...(oauthProvider === "zed" && cred.accountId ? { providerUserId: cred.accountId } : {}),
@@ -717,6 +726,7 @@ export async function getValidAccessTokenSnapshot(
717
726
 
718
727
  /** Providers whose upstream-401 replay path may force a snapshot refresh. */
719
728
  const FORCE_REFRESH_PROVIDERS = new Set([
729
+ "workbuddy-ai",
720
730
  "xai",
721
731
  "github-copilot",
722
732
  "kiro",
@@ -1177,6 +1187,7 @@ function modelDiscoveryTransportSeed(providerName: string, prov: OcxProviderConf
1177
1187
  * `/models` + Bearer with a `{ data: [{ id, owned_by? }] }` response.
1178
1188
  */
1179
1189
  export interface ModelsRequestObservedAuth {
1190
+ readonly workbuddy?: import("./types").WorkBuddyOAuthMetadata;
1180
1191
  readonly oauthApiBaseUrl?: string;
1181
1192
  }
1182
1193
 
@@ -1186,6 +1197,10 @@ export function buildModelsRequest(
1186
1197
  providerName = "",
1187
1198
  observedAuth?: ModelsRequestObservedAuth,
1188
1199
  ): { method?: "POST"; url: string; headers: Record<string, string> } {
1200
+ if (providerName === "workbuddy-ai" || prov.adapter === "workbuddy-ai") {
1201
+ const allowed = mayResolveModelsOAuth(providerName, prov);
1202
+ return { url: WORKBUDDY_CONFIG_URL, headers: workBuddyHeaders(allowed && observedAuth?.workbuddy ? apiKey : undefined, observedAuth?.workbuddy) };
1203
+ }
1189
1204
  const authorizedTarget = captureModelsOAuthTarget(providerName, prov);
1190
1205
  if (prov.authMode === "oauth" && !mayResolveModelsOAuth(providerName, prov)) apiKey = undefined;
1191
1206
  const transportSeed = modelDiscoveryTransportSeed(providerName, prov);
@@ -1672,7 +1687,7 @@ export async function runLogin(
1672
1687
  : existing.accountId && cred.accountId
1673
1688
  ? existing.accountId === cred.accountId
1674
1689
  : existing.email && cred.email
1675
- ? existing.email.toLowerCase() === cred.email.toLowerCase()
1690
+ ? existing.email.trim().toLowerCase() === cred.email.trim().toLowerCase()
1676
1691
  : false;
1677
1692
  if (!identityMatches) {
1678
1693
  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 });
@@ -1,3 +1,4 @@
1
+ import { assertWorkBuddyDestination } from "../providers/workbuddy";
1
2
  import type { OcxProviderConfig } from "../types";
2
3
  import { isBuiltinAnthropicInstanceRow } from "../providers/anthropic-instance";
3
4
  import { resolveProviderModelDiscoveryUrl } from "../providers/model-discovery";
@@ -8,6 +9,10 @@ export function mayResolveModelsOAuth(name: string, provider: OcxProviderConfig
8
9
  // The DSH account token is pinned to its own adapter and fixed DeepSeek hosts. Its catalog is
9
10
  // static, so no discovery path may resolve it, whatever adapter or baseUrl the row was edited to.
10
11
  if (name === "dsh-account") return false;
12
+ if (name === "workbuddy-ai" || provider?.adapter === "workbuddy-ai") {
13
+ if (name !== "workbuddy-ai" || provider?.adapter !== "workbuddy-ai" || provider.disabled) return false;
14
+ try { assertWorkBuddyDestination(provider.baseUrl); return true; } catch { return false; }
15
+ }
11
16
  return name !== "anthropic2" || (provider !== undefined && provider.disabled !== true
12
17
  && isBuiltinAnthropicInstanceRow(name, provider));
13
18
  }
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.
@@ -23,6 +23,7 @@ import { notifyAuthStoreLoaded } from "../lib/decision-quota-sink";
23
23
  import { accountPauseDetailsSchema } from "../config/schema/account-auto-pause";
24
24
  import { normalizeOAuthPlan, mergeOAuthPlan } from "./plan";
25
25
  import { normalizeAnthropicIdentity, matchesAnthropicImport, mergeAnthropicIdentity } from "./anthropic-identity";
26
+ import { normalizeWorkBuddyMetadata } from "../providers/workbuddy";
26
27
  import { findKiroAccountForLogin, withRetainedKiroUser } from "./kiro-account-identity";
27
28
  import { isAnthropicInstanceId, type AnthropicInstanceId } from "../providers/anthropic-instance-id";
28
29
  import { assertNoCrossAnthropicRegistration, assertAnthropicCredentialSource, assertAnthropicInstanceLoginConfig } from "./store-anthropic-instance";
@@ -203,6 +204,7 @@ export function peekOAuthRefreshIntent(provider: string, accountId: string): OAu
203
204
  }
204
205
  export function writeOAuthRefreshIntent(provider: string, accountId: string, generation: string, createdAt = Date.now(), flightId?: string): OAuthRefreshIntent {
205
206
  const dir = getConfigDir();
207
+ assertNotRealHomeUnderTest(dir);
206
208
  if (!existsSync(dir)) mkdirSync(dir, { recursive: true, mode: 0o700 });
207
209
  hardenConfigDir();
208
210
  const intent: OAuthRefreshIntent = {
@@ -353,7 +355,7 @@ export function clearOAuthRefreshIntent(provider: string, accountId: string, gen
353
355
  });
354
356
  }
355
357
  export function credentialGeneration(cred: OAuthCredentials): string {
356
- return createHash("sha256").update(JSON.stringify([cred.refresh, cred.access, cred.expires])).digest("hex");
358
+ return createHash("sha256").update(JSON.stringify([cred.refresh, cred.access, cred.expires, ...(cred.workbuddy ? [cred.workbuddy] : [])])).digest("hex");
357
359
  }
358
360
 
359
361
  /** Load and normalize the OAuth auth store with hardened permissions, and hand the result to the opt-in decision-quota slot (a no-op unless JEV quota signals are active). */
@@ -547,6 +549,8 @@ function normalizeCredential(cred: unknown): OAuthCredentials | null {
547
549
  };
548
550
  if (typeof candidate.email === "string" && candidate.email.length > 0) normalized.email = candidate.email;
549
551
  if (typeof candidate.accountId === "string" && candidate.accountId.length > 0) normalized.accountId = candidate.accountId;
552
+ const workbuddy = normalizeWorkBuddyMetadata(candidate.workbuddy);
553
+ if (workbuddy) normalized.workbuddy = workbuddy;
550
554
  const identity = normalizeAnthropicIdentity(candidate.anthropicIdentity, candidate.access);
551
555
  if (identity) normalized.anthropicIdentity = identity;
552
556
  if (candidate.plan !== undefined) normalized.plan = normalizeOAuthPlan(candidate.plan) ?? null;
@@ -1152,7 +1156,7 @@ export async function upsertCredentialByIdentity(
1152
1156
  return Boolean(
1153
1157
  safe.email
1154
1158
  && account.credential.email
1155
- && account.credential.email.toLowerCase() === safe.email.toLowerCase(),
1159
+ && account.credential.email.trim().toLowerCase() === safe.email.trim().toLowerCase(),
1156
1160
  );
1157
1161
  };
1158
1162
  const existing = safe.accountId
@@ -215,6 +215,13 @@ export async function guardianSweep(nowMs: number = Date.now()): Promise<Guardia
215
215
  if (!record || record.deletedAt != null) continue;
216
216
  const cred = record.credential;
217
217
  if (!cred) continue;
218
+ // A grant the token endpoint already called revoked or expired cannot be refreshed; posting
219
+ // it again only buys another 401 per backoff window (#7592). The OAuth branch above skips
220
+ // needsReauth for the same reason. A re-login or a completed validation rewrites the record
221
+ // without the marker, which is what readmits the account. A source-linked credential spends
222
+ // no grant of its own, so it keeps the sweep, matching the quota probe's hold.
223
+ if (record.lastCodexValidationTerminal === true && record.lastCodexValidationStatus === "failed"
224
+ && !cred.sourceAuthPath) continue;
218
225
  const needsRefresh = cred.expiresAt <= nowMs + horizonMs;
219
226
  const needsWarmup = opts.codexWarmupEnabled
220
227
  && !record.codexValidationPending
@@ -54,7 +54,16 @@ export interface MuseOAuthMetadata {
54
54
  tierName?: string;
55
55
  }
56
56
 
57
+ export interface WorkBuddyOAuthMetadata {
58
+ uid: string;
59
+ domain: string;
60
+ enterpriseId?: string;
61
+ refreshExpiresAt?: number;
62
+ }
63
+
57
64
  export type OAuthCredentials = {
65
+ /** Account-scoped WorkBuddy identity, kept inside the protected credential store. */
66
+ workbuddy?: WorkBuddyOAuthMetadata;
58
67
  refresh: string;
59
68
  access: string;
60
69
  /** Epoch ms after any small provider-specific early-refresh margin; the shared gate adds 1 minute. */
@@ -0,0 +1,139 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { setTimeout as delay } from "node:timers/promises";
3
+ import { readBoundedResponseBody } from "../lib/bounded-body";
4
+ import { WORKBUDDY_ORIGIN, WORKBUDDY_USER_AGENT, workBuddyHeaderValue, workBuddyHeaders } from "../providers/workbuddy";
5
+ import type { OAuthController, OAuthCredentials, WorkBuddyOAuthMetadata } from "./types";
6
+
7
+ type Json = Record<string, unknown>;
8
+ function record(value: unknown): Json {
9
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value as Json : {};
10
+ }
11
+
12
+ export class WorkBuddyApiError extends Error {
13
+ constructor(readonly status: number, readonly code: number) {
14
+ super(`WorkBuddy AI request failed (HTTP ${status}, code ${code})`);
15
+ }
16
+ }
17
+
18
+ /** Fixed-origin, bounded reads; upstream error/profile text never enters diagnostics. */
19
+ export async function workBuddyApi(
20
+ path: string, headers: Record<string, string>, signal?: AbortSignal, post = false, payload: Json = {},
21
+ ): Promise<Json> {
22
+ const timeout = AbortSignal.timeout(15_000);
23
+ const boundedSignal = signal ? AbortSignal.any([signal, timeout]) : timeout;
24
+ const response = await fetch(`${WORKBUDDY_ORIGIN}${path}`, {
25
+ method: post ? "POST" : "GET", redirect: "error", signal: boundedSignal,
26
+ headers: { "Content-Type": "application/json", Accept: "application/json", "User-Agent": WORKBUDDY_USER_AGENT, ...headers },
27
+ ...(post ? { body: JSON.stringify(payload) } : {}),
28
+ }).catch(() => {
29
+ signal?.throwIfAborted();
30
+ throw new Error("WorkBuddy AI request unavailable");
31
+ });
32
+ const body = await readBoundedResponseBody(response, {
33
+ signal: boundedSignal, maxBytes: 256 * 1024, totalTimeoutMs: 15_000, inactivityTimeoutMs: 15_000, fatalUtf8: true,
34
+ }).catch(() => {
35
+ signal?.throwIfAborted();
36
+ throw new Error("WorkBuddy AI response unavailable");
37
+ });
38
+ let env: Json;
39
+ try {
40
+ if (body.oversized || body.truncated || !body.displaySafe) throw new Error();
41
+ env = record(JSON.parse(body.text));
42
+ } catch {
43
+ signal?.throwIfAborted();
44
+ if (!response.ok) throw new WorkBuddyApiError(response.status, -1);
45
+ throw new Error("WorkBuddy AI returned an invalid response");
46
+ }
47
+ const code = typeof env.code === "number" || typeof env.code === "string" && /^\d+$/.test(env.code)
48
+ ? Number(env.code) : NaN;
49
+ if (!response.ok || !Number.isFinite(code) || code !== 0) {
50
+ throw new WorkBuddyApiError(response.status, Number.isSafeInteger(code) ? code : -1);
51
+ }
52
+ return record(env.data);
53
+ }
54
+
55
+ function expiry(got: Json, absolute: string, relative: string, fallback?: number): number | undefined {
56
+ const at = got[absolute], seconds = got[relative];
57
+ if (typeof at === "number" && Number.isSafeInteger(at) && at > 0) return at;
58
+ if (typeof seconds === "number" && Number.isFinite(seconds) && seconds > 0) {
59
+ const result = Date.now() + seconds * 1000;
60
+ if (Number.isSafeInteger(result)) return result;
61
+ }
62
+ return fallback;
63
+ }
64
+
65
+ function credentials(got: Json, metadata: WorkBuddyOAuthMetadata, previous?: OAuthCredentials): OAuthCredentials {
66
+ const access = got.accessToken;
67
+ const refresh = got.refreshToken ?? previous?.refresh;
68
+ const expires = expiry(got, "expiresAt", "expiresIn");
69
+ const domain = got.domain ?? metadata.domain;
70
+ if (!workBuddyHeaderValue(access, 32768) || !workBuddyHeaderValue(refresh, 32768)
71
+ || !expires || expires <= Date.now() || !workBuddyHeaderValue(domain)) throw new Error("WorkBuddy AI returned invalid credentials");
72
+ return {
73
+ access, refresh, expires, source: "oauth",
74
+ // A uid may belong to both a personal plan and a team; they are separate account slots.
75
+ accountId: JSON.stringify([metadata.uid, metadata.enterpriseId ?? ""]),
76
+ workbuddy: { ...metadata, domain,
77
+ refreshExpiresAt: expiry(got, "refreshExpiresAt", "refreshExpiresIn", metadata.refreshExpiresAt) },
78
+ };
79
+ }
80
+
81
+ async function poll(path: string, headers: Record<string, string>, retryCode: number, signal: AbortSignal): Promise<Json> {
82
+ while (true) {
83
+ signal.throwIfAborted();
84
+ try {
85
+ const result = await workBuddyApi(path, headers, signal);
86
+ if (Object.keys(result).length) return result;
87
+ } catch (error) {
88
+ if (!(error instanceof WorkBuddyApiError) || (error.code !== retryCode && ![408, 429].includes(error.status))) throw error;
89
+ }
90
+ await delay(1000, undefined, { signal });
91
+ }
92
+ }
93
+
94
+ export async function loginWorkBuddy(ctrl: OAuthController): Promise<OAuthCredentials> {
95
+ const deadline = AbortSignal.timeout(5 * 60_000);
96
+ const signal = ctrl.signal ? AbortSignal.any([ctrl.signal, deadline]) : deadline;
97
+ signal.throwIfAborted();
98
+ const state = await workBuddyApi("/v2/plugin/auth/state?platform=workbuddy-ai", {
99
+ "X-No-Authorization": "true", "X-No-User-Id": "true",
100
+ "X-No-Enterprise-Id": "true", "X-No-Department-Info": "true",
101
+ }, signal, true);
102
+ if (!workBuddyHeaderValue(state.state, 4096) || typeof state.authUrl !== "string" || state.authUrl.length > 8192) throw new Error("WorkBuddy AI returned an invalid login state");
103
+ let url: URL;
104
+ try { url = new URL(state.authUrl); } catch { throw new Error("WorkBuddy AI returned an invalid login URL"); }
105
+ if (![WORKBUDDY_ORIGIN, "https://www.codebuddy.ai"].includes(url.origin) || url.username || url.password) throw new Error("WorkBuddy AI returned an untrusted login URL");
106
+ url.searchParams.set("version", "2.0.0");
107
+ url.searchParams.set("loginSessionId", randomUUID());
108
+ ctrl.onAuth?.({ url: url.href, instructions: "Sign in to WorkBuddy AI in your browser. OpenCodex will finish automatically." });
109
+ const query = encodeURIComponent(state.state);
110
+ const token = await poll(`/v2/plugin/auth/token?state=${query}`, {}, 11217, signal);
111
+ if (!workBuddyHeaderValue(token.accessToken, 32768)) throw new Error("WorkBuddy AI returned no access token");
112
+ const domain = token.domain ?? "www.workbuddy.ai";
113
+ if (!workBuddyHeaderValue(domain)) throw new Error("WorkBuddy AI returned an invalid account domain");
114
+ const headers = { Authorization: `Bearer ${token.accessToken}`, "X-Domain": domain, "X-No-User-Id": "true", "X-No-Enterprise-Id": "true" };
115
+ const who = await poll(`/v2/plugin/login/account?state=${query}`, headers, 12151, signal);
116
+ if (!workBuddyHeaderValue(who.uid)) throw new Error("WorkBuddy AI account identity unavailable");
117
+ let enterpriseId = who.enterpriseId;
118
+ if (!enterpriseId && typeof who.type === "string" && who.type.toLowerCase() !== "personal") {
119
+ const result = await workBuddyApi("/v2/plugin/accounts", headers, signal);
120
+ const mine = (Array.isArray(result.accounts) ? result.accounts : []).map(record)
121
+ .filter(a => a.uid === who.uid && workBuddyHeaderValue(a.enterpriseId));
122
+ const same = mine.filter(a => a.type === who.type), last = mine.filter(a => a.lastLogin === true);
123
+ enterpriseId = (same.length === 1 ? same[0] : last.length === 1 ? last[0] : mine.length === 1 ? mine[0] : undefined)?.enterpriseId;
124
+ if (!enterpriseId) throw new Error("WorkBuddy AI team identity unavailable; sign in again");
125
+ }
126
+ if (enterpriseId !== undefined && enterpriseId !== "" && !workBuddyHeaderValue(enterpriseId)) throw new Error("WorkBuddy AI returned an invalid team identity");
127
+ return credentials(token, { uid: who.uid, domain, ...(enterpriseId ? { enterpriseId: enterpriseId as string } : {}) });
128
+ }
129
+
130
+ export async function refreshWorkBuddyToken(refresh: string, signal?: AbortSignal, previous?: OAuthCredentials): Promise<OAuthCredentials> {
131
+ const metadata = previous?.workbuddy;
132
+ if (!metadata || !workBuddyHeaderValue(refresh, 32768)) throw new Error("WorkBuddy AI requires a new browser login");
133
+ if (metadata.refreshExpiresAt && metadata.refreshExpiresAt <= Date.now()) throw new Error("WorkBuddy AI refresh token expired; sign in again");
134
+ const headers = workBuddyHeaders(undefined, metadata);
135
+ headers["X-Refresh-Token"] = refresh;
136
+ headers["X-Auth-Refresh-Source"] = "plugin";
137
+ const token = await workBuddyApi("/v2/plugin/auth/token/refresh", headers, signal, true);
138
+ return credentials(token, metadata, previous);
139
+ }
@@ -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
- reasoningDone(item: { itemId: string; signature?: string; redacted?: string[] }): void;
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() { /* nothing retained beyond the driver's frames */ },
253
+ dispose() { releaseReasoningText(); },
202
254
  };
203
255
  }
204
256
 
@@ -0,0 +1,37 @@
1
+ import { createHash } from "node:crypto";
2
+ import { effectiveGoogleMode, getProviderRegistryEntry } from "./registry";
3
+ import type { OAuthCredentials } from "../oauth/types";
4
+ import type { OcxProviderConfig } from "../types";
5
+
6
+ const ARGON_TOKEN = /(?:^|[^a-z0-9])argon(?:[^a-z0-9]|$)/i;
7
+ /** Argon has an independent quota pool, including its native effort-suffixed identities. */
8
+ export function isAntigravityArgonIdentity(identity: string): boolean { return ARGON_TOKEN.test(identity); }
9
+
10
+ /**
11
+ * The OAuth store key whose account both inference and the quota probe resolve. Inference reads
12
+ * `store[route.providerName]` and refreshes through the OAuth definition registered under that same
13
+ * name, and `ocx login` only accepts registered names, so a renamed provider row never authenticates
14
+ * as this account. Evidence is bound to this key alone; a renamed row reads unknown.
15
+ */
16
+ export const ANTIGRAVITY_OAUTH_STORE_KEY = "google-antigravity";
17
+
18
+ /** Accounting and inference must refer to the same OAuth store entry and the supported Antigravity destination. */
19
+ export function antigravityQuotaEligible(name: string, provider: OcxProviderConfig): boolean {
20
+ if (name !== ANTIGRAVITY_OAUTH_STORE_KEY || provider.disabled || provider.authMode !== "oauth"
21
+ || effectiveGoogleMode(name, provider) !== "cloud-code-assist") return false;
22
+ try {
23
+ const url = new URL(provider.baseUrl);
24
+ const expected = new URL(getProviderRegistryEntry(ANTIGRAVITY_OAUTH_STORE_KEY)!.baseUrl);
25
+ return url.href.replace(/\/$/, "") === expected.href.replace(/\/$/, "");
26
+ } catch { return false; }
27
+ }
28
+
29
+ /** Private generation includes the accounting project, not just the rotating bearer. */
30
+ export function antigravityQuotaCredentialGeneration(credential: OAuthCredentials): string {
31
+ return createHash("sha256").update(JSON.stringify([credential.refresh, credential.access, credential.expires, credential.projectId])).digest("hex");
32
+ }
33
+
34
+ /** Immutable instance/destination policy, never returned to decision services. */
35
+ export function antigravityQuotaPolicy(name: string, provider: OcxProviderConfig): string {
36
+ return createHash("sha256").update(JSON.stringify([name, provider])).digest("hex");
37
+ }
@@ -1,9 +1,11 @@
1
1
  /**
2
2
  * Qwen Cloud OpenAI-compatible endpoint presets.
3
3
  * Token plan is the GUI default; pay-as-you-go and custom are selectable.
4
+ * Token plan host per https://docs.qwencloud.com/token-plan/personal/token-plan-personal-quickstart
5
+ * (checked 2026-10-23); a saved older host stays usable as a custom URL.
4
6
  */
5
7
  export const QWEN_CLOUD_TOKEN_PLAN_BASE_URL =
6
- "https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1";
8
+ "https://token-plan.maas.qwencloudapi.com/compatible-mode/v1";
7
9
  export const QWEN_CLOUD_PAYG_BASE_URL =
8
10
  "https://dashscope.aliyuncs.com/compatible-mode/v1";
9
11
 
@@ -23,10 +25,11 @@ export const QWEN_CLOUD_BASE_URL_CHOICES: readonly ProviderBaseUrlChoice[] = [
23
25
 
24
26
  /**
25
27
  * Alibaba Token Plan International (ap-southeast-1) endpoint presets.
26
- * Same product as the Beijing Token Plan but for international accounts.
27
- * Note: ALIBABA_INTL_TOKEN_PLAN_BASE_URL intentionally duplicates
28
- * QWEN_CLOUD_TOKEN_PLAN_BASE_URL — same host, different product branding
29
- * and model lineup. Kept as a separate constant for clarity.
28
+ * Separate regional account and billing destination; both regions offer both tiers.
29
+ * Alibaba Cloud's own quick start still documents this host (checked 2026-10-23):
30
+ * https://www.alibabacloud.com/help/en/model-studio/token-plan-personal-quick-start
31
+ * Qwen Cloud documents a different Token Plan host, so the two constants are
32
+ * independent and must not be assumed equal.
30
33
  */
31
34
  export const ALIBABA_INTL_TOKEN_PLAN_BASE_URL =
32
35
  "https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1";