@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.
- package/gui/dist/assets/App-_MrbTrBe.js +51 -0
- package/gui/dist/assets/{Tray-uqKRqfvw.js → Tray-5PuUalET.js} +1 -1
- package/gui/dist/assets/index-DKrO2eF3.js +86 -0
- package/gui/dist/assets/quota-summary-BCLivc0Q.js +1 -0
- package/gui/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/adapters/anthropic-model-contract.ts +119 -0
- package/src/adapters/anthropic.ts +14 -83
- package/src/adapters/base.ts +2 -0
- package/src/adapters/command-code-project-context.ts +542 -0
- package/src/adapters/command-code-tool-text.ts +99 -67
- package/src/adapters/command-code.ts +5 -1
- package/src/adapters/cursor/catalog.ts +11 -0
- package/src/adapters/cursor/effort-map.ts +3 -0
- package/src/adapters/devin/cloud-direct/catalog.ts +91 -0
- package/src/adapters/devin/cloud-direct/chat.ts +78 -48
- package/src/adapters/devin/cloud-direct/tool-schema.ts +121 -0
- package/src/adapters/devin/context-overflow.ts +75 -0
- package/src/adapters/devin/live-models.ts +174 -13
- package/src/adapters/devin/reasoning-signature.ts +100 -0
- package/src/adapters/devin.ts +310 -117
- package/src/adapters/kiro/payload.ts +10 -10
- package/src/adapters/kiro-images.ts +58 -7
- package/src/adapters/openai-chat/passthrough.ts +3 -1
- package/src/adapters/openai-chat.ts +2 -1
- package/src/adapters/openai-responses/passthrough.ts +2 -1
- package/src/bridge/response-json.ts +15 -1
- package/src/bridge/sse.ts +3 -1
- package/src/claude/intercept/model-bindings.ts +1 -0
- package/src/cli/account-extended.ts +31 -1
- package/src/cli/account.ts +3 -0
- package/src/cli/capabilities.ts +13 -0
- package/src/cli/client-start-fence.ts +36 -0
- package/src/cli/dispatch.ts +3 -2
- package/src/cli/index.ts +98 -117
- package/src/cli/provider-runtime.ts +23 -2
- package/src/cli/provider.ts +3 -0
- package/src/cli/resolve.ts +18 -0
- package/src/cli/start-owner-fence.ts +30 -0
- package/src/cli/stop-approval.ts +13 -1
- package/src/cli/stop-restore.ts +73 -0
- package/src/cli/tray-proxy.ts +221 -18
- package/src/cli/version-skew.ts +25 -23
- package/src/client/runtime.ts +8 -4
- package/src/codex/auth-api/account-list.ts +11 -5
- package/src/codex/auth-api/login-flow.ts +5 -3
- package/src/codex/auth-api/main-account-probe.ts +186 -110
- package/src/codex/auth-api/pool-mode-gate.ts +61 -7
- package/src/codex/auth-api/pool-quota-probe.ts +142 -90
- package/src/codex/auth-api/reset-credit-service.ts +1 -1
- package/src/codex/catalog/gather-capture.ts +1 -0
- package/src/codex/catalog/model-hints.ts +3 -3
- package/src/codex/catalog/provider-models.ts +2 -0
- package/src/codex/low-quota-events.ts +28 -0
- package/src/codex/low-quota-observer.ts +23 -0
- package/src/codex/low-quota-protection.ts +272 -0
- package/src/codex/quota-query-backoff.ts +148 -0
- package/src/codex/quota.ts +7 -2
- package/src/config/diagnostics.ts +10 -1
- package/src/config/load-degrade.ts +17 -0
- package/src/config/mutation-lock.ts +3 -4
- package/src/config/process-state.ts +1 -1
- package/src/config/provider-validation.ts +22 -0
- package/src/config/schema/blocked-model-redirects.ts +17 -0
- package/src/config/schema/config-schema.ts +4 -1
- package/src/config/schema/leaf-validators.ts +33 -2
- package/src/config/serving-runtimes.ts +412 -0
- package/src/generated/compatibility-version.json +211 -115
- package/src/generated/model-metadata.ts +3 -3
- package/src/images/loop.ts +3 -0
- package/src/lib/errors.ts +2 -1
- package/src/lib/handled-signal-exit.ts +23 -0
- package/src/lib/request-failure-model.ts +1 -0
- package/src/lib/self-launch-argv.ts +9 -0
- package/src/lib/shadow-call.ts +5 -2
- package/src/lib/strict-semver.ts +26 -0
- package/src/lib/transient-service-path.ts +16 -0
- package/src/lib/windows-service-wrappers.ts +10 -6
- package/src/lib/winsw.ts +3 -1
- package/src/oauth/anthropic-model-routes.ts +105 -0
- package/src/oauth/anthropic-routing.ts +85 -48
- package/src/oauth/devin.ts +135 -5
- package/src/oauth/index.ts +20 -3
- package/src/oauth/kiro-terminal-failover.ts +23 -9
- package/src/oauth/pool-settings-capability.ts +10 -2
- package/src/oauth/store.ts +1 -1
- package/src/protocols/encoders/adapter-events.ts +3 -1
- package/src/providers/github-copilot-context.ts +27 -0
- package/src/providers/kiro-models.ts +4 -0
- package/src/providers/model-rename-fields.ts +3 -0
- package/src/providers/quota/account-cache.ts +8 -3
- package/src/providers/quota/devin.ts +286 -0
- package/src/providers/quota/vendor-probes-oauth.ts +6 -2
- package/src/providers/quota.ts +1 -1
- package/src/providers/registry/entries-core.ts +1 -1
- package/src/providers/registry/model-seeds.ts +5 -2
- package/src/responses/compaction-images.ts +27 -0
- package/src/router.ts +104 -16
- package/src/routing/analytics.ts +1 -0
- package/src/server/auth-cors.ts +9 -1
- package/src/server/background-lifecycle.ts +21 -10
- package/src/server/chat-completions.ts +2 -1
- package/src/server/chat-native.ts +1 -0
- package/src/server/index.ts +4 -8
- package/src/server/inference/client-encoder-delivery.ts +3 -0
- package/src/server/management/context.ts +3 -0
- package/src/server/management/low-quota-routes.ts +14 -0
- package/src/server/management/oauth-account-routes.ts +21 -1
- package/src/server/management/provider-overwrite-carry.ts +79 -6
- package/src/server/management/provider-routes.ts +41 -28
- package/src/server/management/route-registry.ts +1 -0
- package/src/server/management-api.ts +7 -0
- package/src/server/responses/adapter-continuation.ts +3 -0
- package/src/server/responses/adapter-delivery.ts +3 -0
- package/src/server/responses/adapter-dispatch.ts +20 -4
- package/src/server/responses/antigravity-validation-refusal.ts +20 -0
- package/src/server/responses/core-auth.ts +1 -0
- package/src/server/responses/core-codex-account.ts +1 -0
- package/src/server/responses/core-normalize.ts +4 -0
- package/src/server/responses/passthrough-dispatch.ts +8 -1
- package/src/server/responses/request-sidecar-auth.ts +2 -0
- package/src/server/responses/request-transport.ts +27 -10
- package/src/server/responses/response-effects.ts +4 -1
- package/src/server/responses/run-turn-execution.ts +126 -42
- package/src/server/responses/sidecar-execution.ts +4 -1
- package/src/service/diagnostics.ts +3 -1
- package/src/service/guarded-manager-target.ts +39 -12
- package/src/service/launchd.ts +14 -6
- package/src/service/repair.ts +6 -1
- package/src/service/service-child-ownership.ts +168 -0
- package/src/service/state.ts +22 -4
- package/src/service/systemd.ts +2 -2
- package/src/service/windows-guarded-manager.ts +530 -0
- package/src/service/windows-taskxml.ts +2 -2
- package/src/service.ts +2 -0
- package/src/types/config.ts +26 -0
- package/src/types/provider.ts +12 -0
- package/src/types/request.ts +2 -0
- package/src/types.ts +1 -0
- package/src/usage/expected-prices.ts +13 -0
- package/src/usage/telemetry-contract.ts +1 -0
- package/src/vision/anthropic-describe.ts +2 -1
- package/src/web-search/anthropic-executor.ts +2 -1
- package/src/web-search/loop.ts +1 -0
- package/gui/dist/assets/App-DDbgOTh6.js +0 -51
- package/gui/dist/assets/index-CuaBJkG4.js +0 -86
- package/gui/dist/assets/quota-summary-COppQSVq.js +0 -1
package/src/oauth/index.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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,
|
|
5
|
+
import { eligibleFailoverAccounts, isGenericFailoverProvider,
|
|
6
6
|
GENERIC_OAUTH_MAX_ACCOUNTS_PER_REQUEST } from "./generic-account-failover";
|
|
7
7
|
|
|
8
|
-
/**
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
13
|
-
|
|
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(
|
|
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(
|
|
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
|
}
|
package/src/oauth/store.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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<
|
|
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
|
package/src/providers/quota.ts
CHANGED
|
@@ -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,
|