@bitkyc08/opencodex 2.69.0 → 2.70.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 (147) hide show
  1. package/gui/dist/assets/App-_MrbTrBe.js +51 -0
  2. package/gui/dist/assets/{Tray-uqKRqfvw.js → Tray-5PuUalET.js} +1 -1
  3. package/gui/dist/assets/index-DKrO2eF3.js +86 -0
  4. package/gui/dist/assets/quota-summary-BCLivc0Q.js +1 -0
  5. package/gui/dist/index.html +1 -1
  6. package/package.json +1 -1
  7. package/src/adapters/anthropic-model-contract.ts +119 -0
  8. package/src/adapters/anthropic.ts +14 -83
  9. package/src/adapters/base.ts +2 -0
  10. package/src/adapters/command-code-project-context.ts +542 -0
  11. package/src/adapters/command-code-tool-text.ts +99 -67
  12. package/src/adapters/command-code.ts +5 -1
  13. package/src/adapters/cursor/catalog.ts +11 -0
  14. package/src/adapters/cursor/effort-map.ts +3 -0
  15. package/src/adapters/devin/cloud-direct/catalog.ts +91 -0
  16. package/src/adapters/devin/cloud-direct/chat.ts +78 -48
  17. package/src/adapters/devin/cloud-direct/tool-schema.ts +121 -0
  18. package/src/adapters/devin/context-overflow.ts +75 -0
  19. package/src/adapters/devin/live-models.ts +174 -13
  20. package/src/adapters/devin/reasoning-signature.ts +100 -0
  21. package/src/adapters/devin.ts +310 -117
  22. package/src/adapters/kiro/payload.ts +10 -10
  23. package/src/adapters/kiro-images.ts +58 -7
  24. package/src/adapters/openai-chat/passthrough.ts +3 -1
  25. package/src/adapters/openai-chat.ts +2 -1
  26. package/src/adapters/openai-responses/passthrough.ts +2 -1
  27. package/src/bridge/response-json.ts +15 -1
  28. package/src/bridge/sse.ts +3 -1
  29. package/src/claude/intercept/model-bindings.ts +1 -0
  30. package/src/cli/account-extended.ts +31 -1
  31. package/src/cli/account.ts +3 -0
  32. package/src/cli/capabilities.ts +13 -0
  33. package/src/cli/client-start-fence.ts +36 -0
  34. package/src/cli/dispatch.ts +3 -2
  35. package/src/cli/index.ts +98 -117
  36. package/src/cli/provider-runtime.ts +23 -2
  37. package/src/cli/provider.ts +3 -0
  38. package/src/cli/resolve.ts +18 -0
  39. package/src/cli/start-owner-fence.ts +30 -0
  40. package/src/cli/stop-approval.ts +13 -1
  41. package/src/cli/stop-restore.ts +73 -0
  42. package/src/cli/tray-proxy.ts +221 -18
  43. package/src/cli/version-skew.ts +25 -23
  44. package/src/client/runtime.ts +8 -4
  45. package/src/codex/auth-api/account-list.ts +11 -5
  46. package/src/codex/auth-api/login-flow.ts +5 -3
  47. package/src/codex/auth-api/main-account-probe.ts +186 -110
  48. package/src/codex/auth-api/pool-mode-gate.ts +61 -7
  49. package/src/codex/auth-api/pool-quota-probe.ts +142 -90
  50. package/src/codex/auth-api/reset-credit-service.ts +1 -1
  51. package/src/codex/catalog/gather-capture.ts +1 -0
  52. package/src/codex/catalog/model-hints.ts +3 -3
  53. package/src/codex/catalog/provider-models.ts +2 -0
  54. package/src/codex/low-quota-events.ts +28 -0
  55. package/src/codex/low-quota-observer.ts +23 -0
  56. package/src/codex/low-quota-protection.ts +272 -0
  57. package/src/codex/quota-query-backoff.ts +148 -0
  58. package/src/codex/quota.ts +7 -2
  59. package/src/config/diagnostics.ts +10 -1
  60. package/src/config/load-degrade.ts +17 -0
  61. package/src/config/mutation-lock.ts +3 -4
  62. package/src/config/process-state.ts +1 -1
  63. package/src/config/provider-validation.ts +22 -0
  64. package/src/config/schema/blocked-model-redirects.ts +17 -0
  65. package/src/config/schema/config-schema.ts +4 -1
  66. package/src/config/schema/leaf-validators.ts +33 -2
  67. package/src/config/serving-runtimes.ts +412 -0
  68. package/src/generated/compatibility-version.json +211 -115
  69. package/src/generated/model-metadata.ts +3 -3
  70. package/src/images/loop.ts +3 -0
  71. package/src/lib/errors.ts +2 -1
  72. package/src/lib/handled-signal-exit.ts +23 -0
  73. package/src/lib/request-failure-model.ts +1 -0
  74. package/src/lib/self-launch-argv.ts +9 -0
  75. package/src/lib/shadow-call.ts +5 -2
  76. package/src/lib/strict-semver.ts +26 -0
  77. package/src/lib/transient-service-path.ts +16 -0
  78. package/src/lib/windows-service-wrappers.ts +10 -6
  79. package/src/lib/winsw.ts +3 -1
  80. package/src/oauth/anthropic-model-routes.ts +105 -0
  81. package/src/oauth/anthropic-routing.ts +85 -48
  82. package/src/oauth/devin.ts +135 -5
  83. package/src/oauth/index.ts +20 -3
  84. package/src/oauth/kiro-terminal-failover.ts +23 -9
  85. package/src/oauth/pool-settings-capability.ts +10 -2
  86. package/src/oauth/store.ts +1 -1
  87. package/src/protocols/encoders/adapter-events.ts +3 -1
  88. package/src/providers/github-copilot-context.ts +27 -0
  89. package/src/providers/kiro-models.ts +4 -0
  90. package/src/providers/model-rename-fields.ts +3 -0
  91. package/src/providers/quota/account-cache.ts +8 -3
  92. package/src/providers/quota/devin.ts +286 -0
  93. package/src/providers/quota/vendor-probes-oauth.ts +6 -2
  94. package/src/providers/quota.ts +1 -1
  95. package/src/providers/registry/entries-core.ts +1 -1
  96. package/src/providers/registry/model-seeds.ts +5 -2
  97. package/src/responses/compaction-images.ts +27 -0
  98. package/src/router.ts +104 -16
  99. package/src/routing/analytics.ts +1 -0
  100. package/src/server/auth-cors.ts +9 -1
  101. package/src/server/background-lifecycle.ts +21 -10
  102. package/src/server/chat-completions.ts +2 -1
  103. package/src/server/chat-native.ts +1 -0
  104. package/src/server/index.ts +4 -8
  105. package/src/server/inference/client-encoder-delivery.ts +3 -0
  106. package/src/server/management/context.ts +3 -0
  107. package/src/server/management/low-quota-routes.ts +14 -0
  108. package/src/server/management/oauth-account-routes.ts +21 -1
  109. package/src/server/management/provider-overwrite-carry.ts +79 -6
  110. package/src/server/management/provider-routes.ts +41 -28
  111. package/src/server/management/route-registry.ts +1 -0
  112. package/src/server/management-api.ts +7 -0
  113. package/src/server/responses/adapter-continuation.ts +3 -0
  114. package/src/server/responses/adapter-delivery.ts +3 -0
  115. package/src/server/responses/adapter-dispatch.ts +20 -4
  116. package/src/server/responses/antigravity-validation-refusal.ts +20 -0
  117. package/src/server/responses/core-auth.ts +1 -0
  118. package/src/server/responses/core-codex-account.ts +1 -0
  119. package/src/server/responses/core-normalize.ts +4 -0
  120. package/src/server/responses/passthrough-dispatch.ts +8 -1
  121. package/src/server/responses/request-sidecar-auth.ts +2 -0
  122. package/src/server/responses/request-transport.ts +27 -10
  123. package/src/server/responses/response-effects.ts +4 -1
  124. package/src/server/responses/run-turn-execution.ts +126 -42
  125. package/src/server/responses/sidecar-execution.ts +4 -1
  126. package/src/service/diagnostics.ts +3 -1
  127. package/src/service/guarded-manager-target.ts +39 -12
  128. package/src/service/launchd.ts +14 -6
  129. package/src/service/repair.ts +6 -1
  130. package/src/service/service-child-ownership.ts +168 -0
  131. package/src/service/state.ts +22 -4
  132. package/src/service/systemd.ts +2 -2
  133. package/src/service/windows-guarded-manager.ts +530 -0
  134. package/src/service/windows-taskxml.ts +2 -2
  135. package/src/service.ts +2 -0
  136. package/src/types/config.ts +26 -0
  137. package/src/types/provider.ts +12 -0
  138. package/src/types/request.ts +2 -0
  139. package/src/types.ts +1 -0
  140. package/src/usage/expected-prices.ts +13 -0
  141. package/src/usage/telemetry-contract.ts +1 -0
  142. package/src/vision/anthropic-describe.ts +2 -1
  143. package/src/web-search/anthropic-executor.ts +2 -1
  144. package/src/web-search/loop.ts +1 -0
  145. package/gui/dist/assets/App-DDbgOTh6.js +0 -51
  146. package/gui/dist/assets/index-CuaBJkG4.js +0 -86
  147. package/gui/dist/assets/quota-summary-COppQSVq.js +0 -1
@@ -30,6 +30,7 @@ import {
30
30
  type OAuthCredentialWriteReceipt,
31
31
  type OAuthRefreshIntent,
32
32
  type OAuthRefreshIntentCleanupPending,
33
+ type AuthStore,
33
34
  } from "./store";
34
35
  import { loginXai, refreshXaiToken, XAI_LOCAL_CLI_DETACH_WARNING, XaiTokenRequestError } from "./xai";
35
36
  import { ANTHROPIC_OAUTH_BETA, AnthropicTokenError, loginAnthropic, refreshAnthropicToken } from "./anthropic";
@@ -38,7 +39,7 @@ import { loginNous, NousTokenError, refreshNousToken, clearNousRefreshIntent, Re
38
39
  import { loginChatGPT, refreshChatGPTToken, type ChatGPTLoginFlow } from "./chatgpt";
39
40
  import { loginAntigravity, refreshAntigravityToken } from "./google-antigravity";
40
41
  import { loginCursor, refreshCursorToken } from "./cursor";
41
- import { loginDevin, refreshDevinToken } from "./devin";
42
+ import { assertDevinCliAdoptionOwnership, loginDevin, refreshDevinToken } from "./devin";
42
43
  import { validateDevinApiBaseUrl } from "./devin/api-base";
43
44
  import { loginGithubCopilot, refreshGithubCopilotToken, validateCopilotApiBaseUrl } from "./github-copilot";
44
45
  import { loginCommandCode, refreshCommandCodeToken } from "./command-code";
@@ -192,6 +193,8 @@ interface OAuthProviderDef {
192
193
  refreshToken: string,
193
194
  signal?: AbortSignal,
194
195
  credential?: OAuthCredentials,
196
+ /** Store row being refreshed; passed by the generic lock only. */
197
+ accountId?: string,
195
198
  ): Promise<OAuthCredentials>;
196
199
  /** provider entry written into config.json on first login. */
197
200
  providerConfig: OcxProviderConfig;
@@ -640,6 +643,7 @@ const FORCE_REFRESH_PROVIDERS = new Set([
640
643
  "kiro",
641
644
  "google-antigravity",
642
645
  "orcarouter-oauth",
646
+ "devin",
643
647
  ]);
644
648
 
645
649
  export async function forceRefreshOAuthAccessSnapshot(
@@ -848,7 +852,11 @@ function authoritative(stored:OAuthCredentials,active:boolean,now:()=>number):OA
848
852
  function merged(fresh: OAuthCredentials, previous: OAuthCredentials): OAuthCredentials {
849
853
  return {
850
854
  ...fresh,
851
- source: previous.source === "local-cli" ? "oauth" : fresh.source ?? previous.source ?? "oauth",
855
+ // Shared: a refresh function returns "local-cli" only when the credential it hands back
856
+ // still is the local CLI's (Devin re-reading the CLI file, Meta Muse echoing its durable
857
+ // CLI key). Relabelling that "oauth" would stop the next forced refresh from re-reading it.
858
+ source: fresh.source === "local-cli" ? "local-cli"
859
+ : previous.source === "local-cli" ? "oauth" : fresh.source ?? previous.source ?? "oauth",
852
860
  ...(fresh.projectId === undefined && previous.projectId ? { projectId: previous.projectId } : {}),
853
861
  ...(fresh.apiBaseUrl === undefined && previous.apiBaseUrl ? { apiBaseUrl: previous.apiBaseUrl } : {}),
854
862
  ...(fresh.email === undefined && previous.email ? { email: previous.email } : {}),
@@ -1052,10 +1060,15 @@ export async function refreshGenericAccountWithLock(
1052
1060
  }
1053
1061
  const generation = credentialGeneration(stored);
1054
1062
  try {
1055
- const fresh = merged(await def.refresh(stored.refresh, deps.signal, stored), stored);
1063
+ const refreshed = await def.refresh(stored.refresh, deps.signal, stored, accountId);
1064
+ const fresh = merged(refreshed, stored);
1056
1065
  const outcome = await mergeAccountCredential(provider, accountId, fresh, {
1057
1066
  expectedGeneration: generation,
1058
1067
  afterPrePersistRead: deps.afterPrePersistRead,
1068
+ ...(provider === "devin" ? { assertOwnership: (store: AuthStore) => {
1069
+ if (store[provider]?.accounts.find(row => row.id === accountId)?.paused) throw new OAuthAccountPausedError();
1070
+ assertDevinCliAdoptionOwnership(store, provider, accountId, refreshed);
1071
+ } } : {}),
1059
1072
  });
1060
1073
  if (outcome.superseded) {
1061
1074
  if (outcome.stored.expires > Date.now() + REFRESH_SKEW_MS) return outcome.stored.access;
@@ -1575,6 +1588,10 @@ export function upsertOAuthProvider(config: OcxConfig, provider: string): void {
1575
1588
  // Login used to rebuild the whole row from the preset, so catalog data refreshed
1576
1589
  // immediately. Keep that timing without overwriting unrelated operator-owned fields.
1577
1590
  applyOAuthPresetCatalog(next, providerConfig);
1591
+ // The per-model Copilot tier is operator intent, not account or preset metadata.
1592
+ if (existing?.modelContextTiers !== undefined) {
1593
+ next.modelContextTiers = structuredClone(existing.modelContextTiers);
1594
+ }
1578
1595
  // The original Command Code seed was an implementation-owned static catalog, not an
1579
1596
  // operator opt-out. Promote that exact legacy shape when OAuth login refreshes the row.
1580
1597
  if (provider === "command-code" && existing && isLegacyCommandCodeStaticCatalog(existing)) {
@@ -2,27 +2,41 @@ import type { OAuthAccessSnapshot } from "./index";
2
2
  import type { OcxConfig } from "../types";
3
3
  import { getValidAccessSnapshotForAccount } from "./index";
4
4
  import { credentialGeneration, getAccountCredentialWithStatus, getAccountSet } from "./store";
5
- import { eligibleFailoverAccounts, isGenericOAuthFailoverEnabled,
5
+ import { eligibleFailoverAccounts, isGenericFailoverProvider,
6
6
  GENERIC_OAUTH_MAX_ACCOUNTS_PER_REQUEST } from "./generic-account-failover";
7
7
 
8
- /** Only an unchanged, allowlist-classified dead credential permits this alternate. */
9
- export async function tryKiroAlternateAfterTerminalRefresh(
10
- config: OcxConfig, failedAccountId: string, failedGeneration: string,
8
+ /**
9
+ * Only an unchanged, allowlist-classified dead credential permits this alternate.
10
+ *
11
+ * Consent is the stored-login count, needsReauth rows included: the refused account was
12
+ * marked needsReauth a moment ago, and counting only healthy rows would make a two-account
13
+ * setup look like one exactly when the second account is needed.
14
+ */
15
+ export async function tryAlternateAfterTerminalRefresh(
16
+ config: OcxConfig, providerName: string, failedAccountId: string, failedGeneration: string,
11
17
  ): Promise<OAuthAccessSnapshot | null> {
12
- if (!isGenericOAuthFailoverEnabled(config, "kiro")) return null;
13
- const failed = getAccountCredentialWithStatus("kiro", failedAccountId);
18
+ const provider = config.providers?.[providerName];
19
+ if (!provider || !isGenericFailoverProvider(providerName, provider)) return null;
20
+ const order = getAccountSet(providerName)?.accounts.map(row => row.id) ?? [];
21
+ if (order.length < 2) return null;
22
+ const failed = getAccountCredentialWithStatus(providerName, failedAccountId);
14
23
  if (!failed?.needsReauth || credentialGeneration(failed.credential) !== failedGeneration) return null;
15
- const order = getAccountSet("kiro")?.accounts.map(row => row.id) ?? [];
16
24
  const after = order.indexOf(failedAccountId);
17
25
  if (after < 0) return null;
18
26
  const ring = [...order.slice(after + 1), ...order.slice(0, after)];
19
- const eligible = new Set(eligibleFailoverAccounts("kiro"));
27
+ const eligible = new Set(eligibleFailoverAccounts(providerName));
20
28
  let attempted = 0;
21
29
  for (const id of ring) {
22
30
  if (id === failedAccountId || !eligible.has(id)) continue;
23
31
  if (++attempted >= GENERIC_OAUTH_MAX_ACCOUNTS_PER_REQUEST) break;
24
- try { return await getValidAccessSnapshotForAccount("kiro", id, { requireUsableAccount: true }); }
32
+ try { return await getValidAccessSnapshotForAccount(providerName, id, { requireUsableAccount: true }); }
25
33
  catch { /* Keep the original login-required result if every alternate is stale. */ }
26
34
  }
27
35
  return null;
28
36
  }
37
+
38
+ export function tryKiroAlternateAfterTerminalRefresh(
39
+ config: OcxConfig, failedAccountId: string, failedGeneration: string,
40
+ ): Promise<OAuthAccessSnapshot | null> {
41
+ return tryAlternateAfterTerminalRefresh(config, "kiro", failedAccountId, failedGeneration);
42
+ }
@@ -1,6 +1,8 @@
1
1
  import { isGenericFailoverProvider } from "./generic-account-failover";
2
2
  import { parseAccountPoolStickyLimit, parseAccountPoolStrategy, parseCodexAccountPoolStrategy } from "./pool-kernel";
3
3
  import type { OcxConfig, OcxProviderConfig } from "../types";
4
+ import type { AnthropicModelRoute } from "../types/config";
5
+ import { readAnthropicModelRoutes } from "./anthropic-model-routes";
4
6
 
5
7
  /**
6
8
  * Which pool-settings contract a provider speaks (#695, slice 1).
@@ -48,7 +50,7 @@ export function parseGenericStickyLimit(value: unknown): number | null {
48
50
 
49
51
  /** Fields the unified pool-settings contract can carry, per kind. */
50
52
  export const POOL_SETTINGS_FIELDS = [
51
- "enabled", "strategy", "stickyLimit", "autoSwitchThreshold", "quotaWindow", "maxConcurrentPerAccount",
53
+ "enabled", "strategy", "stickyLimit", "autoSwitchThreshold", "quotaWindow", "maxConcurrentPerAccount", "routes",
52
54
  ] as const;
53
55
  export type PoolSettingsField = typeof POOL_SETTINGS_FIELDS[number];
54
56
 
@@ -81,6 +83,9 @@ export interface PoolSettingsDto {
81
83
  autoSwitchThreshold: number | null;
82
84
  quotaWindow: string | null;
83
85
  maxConcurrentPerAccount: number | null;
86
+ routes: AnthropicModelRoute[] | null;
87
+ /** Present only when stored Anthropic routes fail validation on read. */
88
+ routesError?: string;
84
89
  }
85
90
 
86
91
 
@@ -129,7 +134,7 @@ export function genericPoolSettingsDto(
129
134
  /** Which fields each kind actually honours. Declared, never silently omitted. */
130
135
  const SUPPORTED_BY_KIND: Record<PoolSettingsKind, PoolSettingsField[]> = {
131
136
  codex: ["strategy", "stickyLimit", "autoSwitchThreshold"],
132
- anthropic: ["enabled", "strategy", "stickyLimit", "autoSwitchThreshold", "quotaWindow"],
137
+ anthropic: ["enabled", "strategy", "stickyLimit", "autoSwitchThreshold", "quotaWindow", "routes"],
133
138
  generic: ["enabled", "strategy", "stickyLimit", "autoSwitchThreshold"],
134
139
  };
135
140
 
@@ -159,6 +164,7 @@ export function unifiedPoolSettingsDto(
159
164
  autoSwitchThreshold: parseGenericAutoSwitchThreshold(config.autoSwitchThreshold) ?? 80,
160
165
  quotaWindow: null,
161
166
  maxConcurrentPerAccount: null,
167
+ routes: null,
162
168
  };
163
169
  }
164
170
  if (kind === "anthropic") {
@@ -173,6 +179,7 @@ export function unifiedPoolSettingsDto(
173
179
  autoSwitchThreshold: parseGenericAutoSwitchThreshold(pool.autoSwitchThreshold) ?? 80,
174
180
  quotaWindow: typeof pool.quotaWindow === "string" ? pool.quotaWindow : "five-hour",
175
181
  maxConcurrentPerAccount: null,
182
+ ...readAnthropicModelRoutes(pool.routes),
176
183
  };
177
184
  }
178
185
  const failover = config.providers?.[provider]?.oauthAccountFailover ?? {};
@@ -190,5 +197,6 @@ export function unifiedPoolSettingsDto(
190
197
  autoSwitchThreshold: parseGenericAutoSwitchThreshold(failover.autoSwitchThreshold),
191
198
  quotaWindow: null,
192
199
  maxConcurrentPerAccount: provider === "kiro" ? parseKiroAccountCap(failover.maxConcurrentPerAccount) : null,
200
+ routes: null,
193
201
  };
194
202
  }
@@ -1379,5 +1379,5 @@ export async function markAccountNeedsReauth(
1379
1379
  }, [provider, accountId]);
1380
1380
  }
1381
1381
 
1382
- export async function mergeAccountCredential(provider:string,accountId:string,credential:OAuthCredentials,opts:{expectedGeneration?:string;afterPrePersistRead?:()=>void|Promise<void>}={}):Promise<{superseded:false}|{superseded:true;stored:OAuthCredentials}>{const safe=normalizeCredential(credential);if(!safe)throw new Error("Refusing to persist invalid OAuth credential");return await mutateStore(async store=>{await opts.afterPrePersistRead?.();const account=store[provider]?.accounts.find(x=>x.id===accountId);if(!account)throw new Error(`OAuth account disappeared before persist: ${provider}`);if(opts.expectedGeneration!==undefined&&credentialGeneration(account.credential)!==opts.expectedGeneration)return{superseded:true,stored:account.credential};account.credential=safe;delete account.needsReauth;return{superseded:false};},[provider,accountId,safe,opts.expectedGeneration]);}
1382
+ export async function mergeAccountCredential(provider:string,accountId:string,credential:OAuthCredentials,opts:{expectedGeneration?:string;afterPrePersistRead?:()=>void|Promise<void>;assertOwnership?: (store: AuthStore) => void}={}):Promise<{superseded:false}|{superseded:true;stored:OAuthCredentials}>{const safe=normalizeCredential(credential);if(!safe)throw new Error("Refusing to persist invalid OAuth credential");return await mutateStore(async store=>{await opts.afterPrePersistRead?.();const account=store[provider]?.accounts.find(x=>x.id===accountId);if(!account)throw new Error(`OAuth account disappeared before persist: ${provider}`);if(opts.expectedGeneration!==undefined&&credentialGeneration(account.credential)!==opts.expectedGeneration)return{superseded:true,stored:account.credential};opts.assertOwnership?.(store);account.credential=safe;delete account.needsReauth;return{superseded:false};},[provider,accountId,safe,opts.expectedGeneration]);}
1383
1383
  export async function markAccountNeedsReauthIfGeneration(provider:string,accountId:string,generation:string,writerGeneration=captureConfigGeneration()):Promise<boolean>{const key=oauthAccountKey(provider,accountId);if(writerGeneration<lastReconciledGeneration&&!liveOAuthAccountKeys.has(key))return false;return await mutateStore(store=>{const account=store[provider]?.accounts.find(x=>x.id===accountId);if(!account?.credential||credentialGeneration(account.credential)!==generation)return false;if(writerGeneration<lastReconciledGeneration&&!liveOAuthAccountKeys.has(key))return false;account.needsReauth=true;return true;},[provider,accountId,generation]);}
@@ -136,6 +136,8 @@ export interface ClientEncodeHooks {
136
136
  export interface AdapterEventEncodeOptions {
137
137
  translatorBudget: TranslatorBudget;
138
138
  hideThinkingSummary?: boolean;
139
+ /** Provider policy: suppress raw content-channel reasoning, keep provider-authored summaries. */
140
+ hideRawReasoning?: boolean;
139
141
  toolNsMap?: ReadonlyMap<string, { namespace: string; name: string; freeform?: true }>;
140
142
  declaredToolNames?: ReadonlySet<string>;
141
143
  toolParameterSchemas?: ReadonlyMap<string, Record<string, unknown>>;
@@ -613,7 +615,7 @@ export function encodeAdapterEventStream(
613
615
  break;
614
616
  }
615
617
  case "reasoning_raw_delta": {
616
- if (options.hideThinkingSummary) {
618
+ if (options.hideThinkingSummary || options.hideRawReasoning) {
617
619
  hiddenRawBytes += Buffer.byteLength(event.text);
618
620
  break;
619
621
  }
@@ -0,0 +1,27 @@
1
+ import { modelRecordValue } from "../reasoning-effort";
2
+ import type { OcxProviderConfig } from "../types";
3
+
4
+ export function configuredGithubCopilotContextTier(provider: OcxProviderConfig, modelId: string): "default" | "long_context" | undefined {
5
+ const tier = modelRecordValue(provider.modelContextTiers, modelId);
6
+ return tier === "default" || tier === "long_context" ? tier : undefined;
7
+ }
8
+
9
+ /** Only the routed Copilot provider may receive this nonstandard upstream field. */
10
+ export function applyGithubCopilotContextTier(
11
+ body: unknown, provider: OcxProviderConfig, modelId: string, providerName?: string,
12
+ ): unknown {
13
+ if (!body || typeof body !== "object" || Array.isArray(body)) return body;
14
+ const tier = providerName === "github-copilot" ? configuredGithubCopilotContextTier(provider, modelId) : undefined;
15
+ return tier === undefined ? body : { ...body, contextTier: tier };
16
+ }
17
+
18
+ export function githubCopilotCatalogContextWindow(
19
+ providerName: string, provider: OcxProviderConfig, modelId: string, current: number | undefined,
20
+ ): number | undefined {
21
+ if (providerName !== "github-copilot" || configuredGithubCopilotContextTier(provider, modelId) !== "long_context") return current;
22
+ // The tier choice alone is not evidence of a larger window. An exact per-model declaration
23
+ // from the operator or captured registry is authoritative; it can also cap a live window.
24
+ const supported = provider.modelContextWindows && Object.hasOwn(provider.modelContextWindows, modelId)
25
+ ? provider.modelContextWindows[modelId] : undefined;
26
+ return typeof supported === "number" && Number.isSafeInteger(supported) && supported > 0 ? supported : current;
27
+ }
@@ -7,6 +7,9 @@ export const KIRO_MODELS = [
7
7
  // 260923 preemptive: GPT-6 Sol and Luna (OpenAI announced 2026-09-22) added ahead of this provider's own catalog; mirrors the GPT-5.6 Sol/Luna rows. Calls fail upstream until Kiro ships the models.
8
8
  "gpt-6-sol",
9
9
  "gpt-6-luna",
10
+ // 260929 preemptive: Claude Sonnet 5.5 added ahead of Kiro's catalog (kiro.dev did not list it on
11
+ // 2026-09-29). Mirrors claude-sonnet-5; calls fail upstream until Kiro ships the model.
12
+ "claude-sonnet-5.5",
10
13
  "claude-sonnet-5",
11
14
  // 260923 preemptive: Claude Opus 5.5 added ahead of Kiro's catalog (kiro.dev did not list it on
12
15
  // 2026-09-23). Mirrors claude-opus-5; calls fail upstream until Kiro ships the model.
@@ -37,6 +40,7 @@ export const KIRO_MODEL_CONTEXT_WINDOWS: Record<string, number> = {
37
40
  "gpt-5.6-luna": 1_000_000,
38
41
  "gpt-6-sol": 272_000,
39
42
  "gpt-6-luna": 272_000,
43
+ "claude-sonnet-5.5": 1_000_000,
40
44
  "claude-sonnet-5": 1_000_000,
41
45
  "claude-opus-5.5": 1_000_000,
42
46
  "claude-opus-5": 1_000_000,
@@ -10,6 +10,7 @@ export const PROVIDER_MODEL_RENAME_ROLES = {
10
10
  defaultAliases: "none",
11
11
  adapter: "none",
12
12
  codexToolMode: "none",
13
+ projectContext: "none",
13
14
  requestPacing: "nested-models",
14
15
  mcpMaxTools: "none",
15
16
  mcpMaxSchemaBytes: "none",
@@ -57,6 +58,7 @@ export const PROVIDER_MODEL_RENAME_ROLES = {
57
58
  modelPreset: "none",
58
59
  contextWindow: "none",
59
60
  modelContextWindows: "record",
61
+ modelContextTiers: "record",
60
62
  modelInputModalities: "record",
61
63
  modelCapabilities: "record",
62
64
  modelMaxInputTokens: "record",
@@ -120,6 +122,7 @@ export const PROVIDER_MODEL_RENAME_ROLES = {
120
122
  preserveReasoningContentModels: "list",
121
123
  requiresReasoningPlaceholderModels: "list",
122
124
  showThinkingSummary: "none",
125
+ hideRawReasoning: "none",
123
126
  retryOn429: "none",
124
127
  transientRetryOn5xx: "none",
125
128
  retryOnReset: "none",
@@ -180,7 +180,8 @@ export function supportsPerAccountQuota(provider: string): boolean {
180
180
  }
181
181
 
182
182
  export function explicitAccountReader(provider: string): boolean {
183
- return provider === "xai" || provider === "cursor" || provider === "kimi" || provider === "command-code";
183
+ return provider === "xai" || provider === "cursor" || provider === "kimi" || provider === "command-code"
184
+ || provider === "devin";
184
185
  }
185
186
 
186
187
  export function providerOAuthAccountQuotaMode(provider: string): AccountQuotaMode {
@@ -479,6 +480,9 @@ export function quotaCredentialIdentity(provider: string, accountId: string, cre
479
480
  provider, accountId, credential.access, credential.refresh, credential.expires,
480
481
  credential.accountId, credential.projectId, credential.source,
481
482
  target.adapter, target.baseUrl, target.authMode, target.disabled === true,
483
+ // Only credentials that carry their own endpoint (Devin tenants) extend the identity, so
484
+ // every other provider's existing cache keys stay valid.
485
+ ...(provider === "devin" && credential.apiBaseUrl ? [credential.apiBaseUrl] : []),
482
486
  ])).digest("hex");
483
487
  }
484
488
 
@@ -486,6 +490,7 @@ export function explicitQuotaDestination(provider: string, config: OcxProviderCo
486
490
  if (config.disabled === true || config.authMode !== "oauth") return false;
487
491
  if (provider === "kimi") return isCanonicalKimiCodeBaseUrl(config.baseUrl);
488
492
  if (provider === "command-code") return isCanonicalCommandCodeBaseUrl(config.baseUrl);
489
- // These readers use fixed canonical billing origins, never config.baseUrl.
490
- return provider === "xai" || provider === "cursor";
493
+ // These readers use fixed canonical billing origins, never config.baseUrl. Devin reads
494
+ // the credential's own allowlisted api-server host instead (fetchDevinQuota revalidates it).
495
+ return provider === "xai" || provider === "cursor" || provider === "devin";
491
496
  }
@@ -0,0 +1,286 @@
1
+ /**
2
+ * Devin (Cognition) account quota from `SeatManagementService/GetUserStatus`.
3
+ *
4
+ * Cognition has no REST usage endpoint; plan, credit balances and the dated quota windows
5
+ * all come back from this one unary Connect RPC. The request carries the long-lived api_key
6
+ * inside the protobuf Metadata, so the host passes the same allowlist every other Devin RPC
7
+ * uses, redirects are refused, and no response body is ever echoed.
8
+ *
9
+ * Field map, confirmed against a live account:
10
+ *
11
+ * GetUserStatusResponse { #1 UserStatus, #2 PlanInfo }
12
+ * UserStatus { #10 teams_tier, #13 PlanStatus }
13
+ * PlanStatus { #1 PlanInfo, #3 plan_end (Timestamp),
14
+ * #4 available_flex, #5 used_flow, #6 used_prompt, #7 used_flex,
15
+ * #8 available_prompt, #9 available_flow (int32; -1 = unlimited),
16
+ * #14 daily_remaining_percent, #15 weekly_remaining_percent,
17
+ * #16 overage_balance_micros (int64, may be negative),
18
+ * #17 daily_reset_unix, #18 weekly_reset_unix }
19
+ * PlanInfo { #1 teams_tier, #2 plan_name, #13 monthly_flow, #35 billing_strategy,
20
+ * #36 hide_daily_quota, #37 hide_weekly_quota }
21
+ */
22
+ import { buildMetadata } from "../../adapters/devin/cloud-direct/metadata";
23
+ import { encodeMessage } from "../../adapters/devin/cloud-direct/wire";
24
+ import { resolveDevinApiBaseUrl, validateDevinApiBaseUrl } from "../../oauth/devin/api-base";
25
+ import { readBoundedResponseBytes } from "../../lib/bounded-body";
26
+ import { epochMillis, QUOTA_RESPONSE_MAX_BYTES, REQUEST_TIMEOUT_MS } from "../quota-wire";
27
+ import type { ProviderQuota, ProviderQuotaWindow } from "../quota-types";
28
+ import { AUTHORITATIVE_EMPTY_QUOTA, report, TERMINAL_QUOTA_FAILURE, type ProviderQuotaProbeResult } from "./report-cache";
29
+
30
+ const GET_USER_STATUS_PATH = "/exa.seat_management_pb.SeatManagementService/GetUserStatus";
31
+ const BILLING_STRATEGY_CREDITS = 1;
32
+ const RESPONSE_WIRES = new Map([[1, 2], [2, 2]]);
33
+ const USER_WIRES = new Map([[10, 0], [13, 2]]);
34
+ const STATUS_WIRES = new Map<number, number>([[1, 2], [3, 2], ...[4, 5, 6, 7, 8, 9, 14, 15, 16, 17, 18].map(n => [n, 0] as const)]);
35
+ const PLAN_WIRES = new Map([[1, 0], [2, 2], [13, 0], [35, 0], [36, 0], [37, 0]]);
36
+ const TIMESTAMP_WIRES = new Map([[1, 0], [2, 0]]);
37
+
38
+ export interface DevinPlanInfo {
39
+ teamsTier: number;
40
+ planName: string;
41
+ monthlyFlowCredits: number;
42
+ billingStrategy: number;
43
+ hideDailyQuota: boolean;
44
+ hideWeeklyQuota: boolean;
45
+ }
46
+
47
+ export interface DevinUserStatus {
48
+ plan: DevinPlanInfo;
49
+ planEndMs?: number;
50
+ usedPromptCredits: number;
51
+ availablePromptCredits: number;
52
+ usedFlowCredits: number;
53
+ availableFlowCredits: number;
54
+ usedFlexCredits: number;
55
+ availableFlexCredits: number;
56
+ dailyRemainingPercent: number;
57
+ weeklyRemainingPercent: number;
58
+ dailyResetMs?: number;
59
+ weeklyResetMs?: number;
60
+ overageBalanceMicros: number;
61
+ promptCreditBalancePresent: boolean;
62
+ }
63
+
64
+ type Fields = Map<number, bigint | Buffer>;
65
+
66
+ /**
67
+ * Last occurrence wins. Parse locally so an overlong varint, truncated field, or wrong wire
68
+ * type for a known status field cannot turn a malformed credential-bearing RPC into quota.
69
+ */
70
+ function fieldsOf(buf: Buffer | undefined, known: Map<number, number>): Fields | null {
71
+ const out: Fields = new Map();
72
+ if (!buf) return out;
73
+ let offset = 0;
74
+ const varint = (): bigint | null => {
75
+ let value = 0n;
76
+ for (let byte = 0; byte < 10; byte++) {
77
+ if (offset >= buf.length) return null;
78
+ const part = buf[offset++];
79
+ if (byte === 9 && part > 1) return null;
80
+ value |= BigInt(part & 0x7f) << BigInt(byte * 7);
81
+ if (!(part & 0x80)) return value;
82
+ }
83
+ return null;
84
+ };
85
+ while (offset < buf.length) {
86
+ const tag = varint();
87
+ if (tag === null || tag > 0xffffffffn) return null;
88
+ const num = Number(tag >> 3n);
89
+ const wire = Number(tag & 7n);
90
+ if (num === 0 || (known.has(num) && known.get(num) !== wire)) return null;
91
+ if (wire === 0) {
92
+ const value = varint();
93
+ if (value === null) return null;
94
+ out.set(num, value);
95
+ } else if (wire === 1 || wire === 5) {
96
+ const size = wire === 1 ? 8 : 4;
97
+ if (size > buf.length - offset) return null;
98
+ out.set(num, buf.subarray(offset, offset + size));
99
+ offset += size;
100
+ } else if (wire === 2) {
101
+ const length = varint();
102
+ if (length === null || length > BigInt(buf.length - offset)) return null;
103
+ const end = offset + Number(length);
104
+ out.set(num, buf.subarray(offset, end));
105
+ offset = end;
106
+ } else return null;
107
+ }
108
+ return out;
109
+ }
110
+
111
+ /** int32/int64 on the wire are two's-complement varints; -1 arrives as 2^64-1. */
112
+ function int(fields: Fields, num: number): number {
113
+ const value = fields.get(num);
114
+ return typeof value === "bigint" ? Number(BigInt.asIntN(64, value)) : 0;
115
+ }
116
+
117
+ function sub(fields: Fields, num: number): Buffer | undefined {
118
+ const value = fields.get(num);
119
+ return Buffer.isBuffer(value) ? value : undefined;
120
+ }
121
+
122
+ /** Undefined for an absent timestamp; null for one that is present but malformed. */
123
+ function timestampMs(buf: Buffer | undefined): number | undefined | null {
124
+ if (!buf) return undefined;
125
+ const f = fieldsOf(buf, TIMESTAMP_WIRES);
126
+ return f ? epochMillis(int(f, 1)) : null;
127
+ }
128
+
129
+ /** Null when neither plan copy is present: a zero-valued plan would read as an exhausted account. */
130
+ function decodePlanInfo(buf: Buffer | undefined): DevinPlanInfo | null {
131
+ if (!buf) return null;
132
+ const f = fieldsOf(buf, PLAN_WIRES);
133
+ if (!f) return null;
134
+ return {
135
+ teamsTier: int(f, 1),
136
+ planName: sub(f, 2)?.toString("utf8").trim() ?? "",
137
+ monthlyFlowCredits: int(f, 13),
138
+ billingStrategy: int(f, 35),
139
+ hideDailyQuota: int(f, 36) === 1,
140
+ hideWeeklyQuota: int(f, 37) === 1,
141
+ };
142
+ }
143
+
144
+ /** Null when the response carries no PlanStatus, which is not something a mapper can use. */
145
+ export function decodeDevinUserStatus(buf: Buffer): DevinUserStatus | null {
146
+ const response = fieldsOf(buf, RESPONSE_WIRES);
147
+ const user = response && fieldsOf(sub(response, 1), USER_WIRES);
148
+ const statusBuf = user && sub(user, 13);
149
+ if (!statusBuf) return null;
150
+ const status = fieldsOf(statusBuf, STATUS_WIRES);
151
+ if (!status) return null;
152
+ // The top-level PlanInfo is authoritative; the nested copy covers servers that omit it.
153
+ const plan = decodePlanInfo(sub(response, 2) ?? sub(status, 1));
154
+ if (!plan) return null;
155
+ if (!plan.teamsTier) plan.teamsTier = int(user, 10);
156
+ const planEndMs = timestampMs(sub(status, 3));
157
+ if (planEndMs === null) return null;
158
+ const dailyResetMs = epochMillis(int(status, 17));
159
+ const weeklyResetMs = epochMillis(int(status, 18));
160
+ return {
161
+ plan,
162
+ ...(planEndMs !== undefined ? { planEndMs } : {}),
163
+ usedPromptCredits: int(status, 6),
164
+ availablePromptCredits: int(status, 8),
165
+ usedFlowCredits: int(status, 5),
166
+ availableFlowCredits: int(status, 9),
167
+ usedFlexCredits: int(status, 7),
168
+ availableFlexCredits: int(status, 4),
169
+ dailyRemainingPercent: int(status, 14),
170
+ weeklyRemainingPercent: int(status, 15),
171
+ ...(dailyResetMs !== undefined ? { dailyResetMs } : {}),
172
+ ...(weeklyResetMs !== undefined ? { weeklyResetMs } : {}),
173
+ overageBalanceMicros: int(status, 16),
174
+ // proto3 omits a zero balance, so an exhausted pool arrives as `used` alone. Only a
175
+ // status with no balance field at all carries no credit evidence.
176
+ promptCreditBalancePresent: [4, 6, 7, 8].some((field) => status.has(field)),
177
+ };
178
+ }
179
+
180
+ function usedPercent(used: number, available: number): number | undefined {
181
+ // A negative balance is the unlimited sentinel; nothing to measure against.
182
+ if (available < 0 || used < 0 || used + available <= 0) return undefined;
183
+ return Math.min(100, (used / (used + available)) * 100);
184
+ }
185
+
186
+ function remainingToUsed(remaining: number): number {
187
+ return 100 - Math.max(0, Math.min(100, remaining));
188
+ }
189
+
190
+ /**
191
+ * Only DATED percent windows whose reset is still ahead are published. A credit-billed plan
192
+ * leaves the percent fields at their zero default, and a reset already in the past describes
193
+ * a window that has rolled over; either would read as a fully spent window and mark the
194
+ * account exhausted for routing.
195
+ *
196
+ * The credit pool is published only for a credit-billed plan, or for an unknown strategy
197
+ * with both reset fields absent. Expired dated windows still identify a quota plan.
198
+ * A quota-billed plan still reports credit balances, and a zero balance there does not
199
+ * gate anything.
200
+ *
201
+ * Flow credits are decoded but not published. They meter agent tool actions, not chat
202
+ * turns, and account ranking reads the fullest custom window as the account's limit, so an
203
+ * empty flow balance would retire an account that can still serve chat.
204
+ *
205
+ * Credits are measured against the server's own balance rather than the plan grant, so
206
+ * top-ups count. Flex credits back prompt credits once those run out, so both share one
207
+ * pool: an account with prompt credits spent and flex remaining can still serve.
208
+ */
209
+ export function devinQuotaFromStatus(status: DevinUserStatus, now = Date.now()): ProviderQuota {
210
+ const quota: ProviderQuota = { updatedAt: now };
211
+ const customWindows: ProviderQuotaWindow[] = [];
212
+ const ahead = (resetMs: number | undefined): resetMs is number => resetMs !== undefined && resetMs > now;
213
+ if (!status.plan.hideDailyQuota && ahead(status.dailyResetMs)) {
214
+ customWindows.push({ label: "Daily", percent: remainingToUsed(status.dailyRemainingPercent), resetAt: status.dailyResetMs });
215
+ }
216
+ if (!status.plan.hideWeeklyQuota && ahead(status.weeklyResetMs)) {
217
+ quota.weeklyPercent = remainingToUsed(status.weeklyRemainingPercent);
218
+ quota.weeklyResetAt = status.weeklyResetMs;
219
+ }
220
+ const strategy = status.plan.billingStrategy;
221
+ const creditBilled = strategy === BILLING_STRATEGY_CREDITS
222
+ || (strategy === 0 && status.dailyResetMs === undefined && status.weeklyResetMs === undefined);
223
+ const available = status.availablePromptCredits + status.availableFlexCredits;
224
+ const credits = !creditBilled || !status.promptCreditBalancePresent
225
+ || status.availablePromptCredits < 0 || status.availableFlexCredits < 0
226
+ || status.usedPromptCredits < 0 || status.usedFlexCredits < 0
227
+ ? undefined
228
+ // A credit-billed plan with nothing used and nothing left has no balance to serve from;
229
+ // leaving it unmeasured would rank it as untested headroom.
230
+ : available === 0 ? 100
231
+ : usedPercent(status.usedPromptCredits + status.usedFlexCredits, available);
232
+ if (credits !== undefined) {
233
+ quota.monthlyPercent = credits;
234
+ if (status.planEndMs !== undefined) quota.monthlyResetAt = status.planEndMs;
235
+ }
236
+ if (customWindows.length > 0) quota.customWindows = customWindows;
237
+ return quota;
238
+ }
239
+
240
+ /**
241
+ * Probe one Devin account. `null` keeps the last-good row (transient failure);
242
+ * `TERMINAL_QUOTA_FAILURE` means the credential or contract is rejected.
243
+ */
244
+ export async function fetchDevinQuota(provider: string, apiKey: string, apiBaseUrl: string | undefined, configuredBaseUrl?: string): Promise<ProviderQuotaProbeResult> {
245
+ const base = validateDevinApiBaseUrl(apiBaseUrl)
246
+ ?? (configuredBaseUrl !== undefined || apiBaseUrl === undefined ? resolveDevinApiBaseUrl(configuredBaseUrl) : undefined);
247
+ if (!base || !apiKey.trim()) return null;
248
+ const metadata = buildMetadata({
249
+ apiKey,
250
+ sessionId: crypto.randomUUID(),
251
+ requestId: BigInt(Date.now()),
252
+ triggerId: crypto.randomUUID(),
253
+ });
254
+ try {
255
+ const deadline = AbortSignal.timeout(REQUEST_TIMEOUT_MS);
256
+ const response = await fetch(`${base}${GET_USER_STATUS_PATH}`, {
257
+ method: "POST",
258
+ headers: { "Content-Type": "application/proto", "Connect-Protocol-Version": "1" },
259
+ // GetUserStatusRequest { #1 metadata }
260
+ body: new Uint8Array(encodeMessage(1, metadata)),
261
+ // The body carries the api_key; a redirect would replay it at another host.
262
+ redirect: "error",
263
+ signal: deadline,
264
+ });
265
+ if (!response.ok) {
266
+ void response.body?.cancel().catch(() => undefined);
267
+ // Only 401 proves the key itself is refused. A 403 can scope this one RPC away from a
268
+ // key that still serves chat, so it keeps the last-good reading like other failures.
269
+ return response.status === 401 ? TERMINAL_QUOTA_FAILURE : null;
270
+ }
271
+ const body = await readBoundedResponseBytes(response, {
272
+ signal: deadline,
273
+ maxBytes: QUOTA_RESPONSE_MAX_BYTES,
274
+ inactivityTimeoutMs: REQUEST_TIMEOUT_MS,
275
+ });
276
+ if (body.oversized) return null;
277
+ // Inside the try: a truncated varint throws, and a malformed body must keep last-good.
278
+ const status = decodeDevinUserStatus(Buffer.from(body.bytes.buffer, body.bytes.byteOffset, body.bytes.byteLength));
279
+ if (!status) return null;
280
+ // A decoded status with nothing measurable (an unlimited plan, hidden windows) is an
281
+ // authoritative answer, not a failed probe, so it must replace any last-good row.
282
+ return report(provider, "devin:user-status", devinQuotaFromStatus(status)) ?? AUTHORITATIVE_EMPTY_QUOTA;
283
+ } catch {
284
+ return null;
285
+ }
286
+ }
@@ -18,6 +18,7 @@ import { aggregateCodexPoolCapacity, CODEX_CAPACITY_MAX_QUOTA_AGE_MS, type Codex
18
18
  import { asRecord, normalizePercent, normalizeResetAt, readQuotaJson, REQUEST_TIMEOUT_MS, toFiniteNumber } from "../quota-wire";
19
19
  import { providerCodexAccountMode } from "../registry";
20
20
  import {
21
+ TERMINAL_QUOTA_FAILURE,
21
22
  hasQuotaRows,
22
23
  providerLabel,
23
24
  providerQuotaFromCodexQuota,
@@ -25,6 +26,7 @@ import {
25
26
  report,
26
27
  tagNativeMainReport,
27
28
  type CodexAuthAccountsSnapshotPromise,
29
+ type ProviderQuotaProbeResult,
28
30
  type ProviderQuotaReport,
29
31
  } from "./report-cache";
30
32
  import {
@@ -46,9 +48,11 @@ export async function fetchChatGptForwardQuota(
46
48
  providerConfig: OcxProviderConfig,
47
49
  forceRefresh: boolean,
48
50
  prefetchedSnapshot?: CodexAuthAccountsSnapshotPromise,
49
- ): Promise<ProviderQuotaReport | null> {
51
+ ): Promise<ProviderQuotaProbeResult> {
50
52
  if (providerCodexAccountMode(provider, providerConfig) === "direct") {
51
- const snapshot = await fetchMainAccountInfoSnapshot(forceRefresh);
53
+ const snapshot = await fetchMainAccountInfoSnapshot(forceRefresh, config);
54
+ // A parsed return from a replaced credential cannot retain an older cached report either.
55
+ if (snapshot.infoUnpublished) return TERMINAL_QUOTA_FAILURE;
52
56
  const quota = providerQuotaFromCodexQuota(snapshot.info.quota);
53
57
  if (quota) quota.updatedAt = Date.now();
54
58
  return quota
@@ -1,4 +1,3 @@
1
-
2
1
  import { listCodexAuthAccountsSnapshot } from "../codex/auth-api";
3
2
  import { resolveEnvValue } from "../config";
4
3
  import { getAccountCredential, getAccountSet } from "../oauth/store";
@@ -363,6 +362,7 @@ async function readExplicitAccountQuota(provider: string, accountId: string, con
363
362
  case "cursor": result = await fetchCursorQuota(provider, accessToken); break;
364
363
  case "kimi": result = await fetchKimiQuota(provider, config, accessToken); break;
365
364
  case "command-code": result = await fetchCommandCodeQuota(provider, config, accessToken); break;
365
+ case "devin": result = await (await import("./quota/devin")).fetchDevinQuota(provider, accessToken, credential.apiBaseUrl, config.baseUrl); break;
366
366
  default: return null;
367
367
  }
368
368
  return { result, identity, isCurrent };
@@ -190,7 +190,7 @@ export const PROVIDER_REGISTRY_CORE: readonly ProviderRegistryEntry[] = [
190
190
  // (it is the current catalog, so its default ordering wins), then the ids
191
191
  // only the old devin entry carried. Degraded-mode seed only either way —
192
192
  // `liveModels` discovers the account's real roster.
193
- models: ["swe-2", "swe-1-7", "gpt-5-6-sol", "gpt-6-astra", "gpt-6-sol", "gpt-6-luna", "claude-opus-5-5", "claude-opus-5", "claude-fable-5-1", "claude-sonnet-5", "glm-5-3", "kimi-k3", "gemini-3-8-flash", "grok-4-6", "grok-4-7", "swe-1-7-lightning", "gpt-5-6-luna", "gpt-5-6-terra", "claude-opus-4-8", "glm-5-2", "kimi-k2-7", "grok-4-5"],
193
+ models: ["swe-2", "swe-1-7", "gpt-5-6-sol", "gpt-6-astra", "gpt-6-sol", "gpt-6-luna", "claude-opus-5-5", "claude-opus-5", "claude-fable-5-1", "claude-sonnet-5-5", "claude-sonnet-5", "glm-5-3", "kimi-k3", "gemini-3-8-flash", "grok-4-6", "grok-4-7", "swe-1-7-lightning", "gpt-5-6-luna", "gpt-5-6-terra", "claude-opus-4-8", "glm-5-2", "kimi-k2-7", "grok-4-5"],
194
194
  liveModels: true,
195
195
  defaultModel: "swe-2",
196
196
  modelContextWindows: DEVIN_MODEL_CONTEXT_WINDOWS,