@bitkyc08/opencodex 2.10.0 → 2.10.1
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/AGENTS_INSTALL.md +77 -0
- package/README.md +4 -10
- package/bin/ocx.mjs +71 -18
- package/gui/dist/assets/index-Cd6_PBKn.css +1 -0
- package/gui/dist/assets/index-ChZQsmBY.js +70 -0
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/alibaba-color.svg +1 -1
- package/gui/dist/provider-icons/antigravity-color.svg +1 -1
- package/gui/dist/provider-icons/claude-color.svg +1 -1
- package/gui/dist/provider-icons/cline-color.svg +16 -0
- package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -1
- package/gui/dist/provider-icons/copilot-color.svg +1 -1
- package/gui/dist/provider-icons/cursor-color.svg +1 -1
- package/gui/dist/provider-icons/deepseek-color.svg +1 -1
- package/gui/dist/provider-icons/firepass-color.svg +1 -1
- package/gui/dist/provider-icons/fireworks-color.svg +1 -1
- package/gui/dist/provider-icons/gemini-color.svg +1 -1
- package/gui/dist/provider-icons/github-copilot-color.svg +1 -1
- package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -1
- package/gui/dist/provider-icons/grok.svg +1 -1
- package/gui/dist/provider-icons/groq-color.svg +1 -1
- package/gui/dist/provider-icons/huggingface-color.svg +1 -1
- package/gui/dist/provider-icons/kimi-color.svg +1 -1
- package/gui/dist/provider-icons/kiro-color.svg +2 -2
- package/gui/dist/provider-icons/lm-studio-color.svg +1 -1
- package/gui/dist/provider-icons/mistral-color.svg +1 -1
- package/gui/dist/provider-icons/moonshot-color.svg +1 -1
- package/gui/dist/provider-icons/nvidia-color.svg +1 -1
- package/gui/dist/provider-icons/ollama-color.svg +1 -1
- package/gui/dist/provider-icons/openai.svg +1 -1
- package/gui/dist/provider-icons/opencode.svg +2 -1
- package/gui/dist/provider-icons/openrouter-color.svg +1 -1
- package/gui/dist/provider-icons/pi.svg +2 -2
- package/gui/dist/provider-icons/qianfan-color.svg +1 -1
- package/gui/dist/provider-icons/qwen-portal-color.svg +1 -1
- package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -1
- package/gui/dist/provider-icons/vllm-color.svg +1 -1
- package/gui/dist/provider-icons/xiaomi-color.svg +1 -1
- package/package.json +8 -4
- package/src/adapters/anthropic.ts +208 -14
- package/src/adapters/base.ts +16 -5
- package/src/adapters/cursor/effort-map.ts +3 -2
- package/src/adapters/cursor/framing.ts +39 -0
- package/src/adapters/cursor/live-transport.ts +105 -95
- package/src/adapters/cursor/native-exec.ts +32 -6
- package/src/adapters/cursor/protobuf-request.ts +20 -15
- package/src/adapters/cursor/request-builder.ts +21 -7
- package/src/adapters/cursor/types.ts +7 -0
- package/src/adapters/google-antigravity-replay.ts +237 -21
- package/src/adapters/google-truncation.ts +11 -0
- package/src/adapters/google.ts +50 -9
- package/src/adapters/identity.ts +39 -6
- package/src/adapters/kiro-errors.ts +11 -0
- package/src/adapters/kiro-events.ts +19 -1
- package/src/adapters/kiro-thinking.ts +10 -2
- package/src/adapters/kiro-tools.ts +10 -1
- package/src/adapters/kiro.ts +37 -11
- package/src/adapters/openai-chat.ts +284 -83
- package/src/adapters/openai-responses.ts +182 -24
- package/src/bridge.ts +177 -7
- package/src/chat/outbound.ts +78 -23
- package/src/claude/agents-inject.ts +27 -5
- package/src/claude/inbound.ts +11 -1
- package/src/claude/model-info.ts +13 -10
- package/src/claude/outbound.ts +17 -0
- package/src/cli/account-api.ts +24 -0
- package/src/cli/account-auth.ts +31 -6
- package/src/cli/account-main.ts +317 -0
- package/src/cli/account.ts +5 -0
- package/src/cli/claude.ts +2 -1
- package/src/cli/doctor.ts +93 -22
- package/src/cli/export-command.ts +26 -12
- package/src/cli/help.ts +8 -6
- package/src/cli/index.ts +56 -22
- package/src/cli/integrations.ts +84 -1
- package/src/cli/observe.ts +54 -1
- package/src/cli/opencode.ts +2 -1
- package/src/cli/provider-runtime.ts +18 -1
- package/src/cli/route-policy.ts +92 -0
- package/src/cli/runtime-api.ts +6 -3
- package/src/cli/star-prompt.ts +71 -15
- package/src/cli/status.ts +1 -1
- package/src/cli/v2.ts +36 -9
- package/src/clients/config-export.ts +687 -10
- package/src/codex/account-lifecycle.ts +30 -5
- package/src/codex/account-usability.ts +22 -2
- package/src/codex/admission.ts +255 -0
- package/src/codex/auth-api.ts +427 -140
- package/src/codex/auth-context.ts +155 -30
- package/src/codex/autostart-health.ts +8 -1
- package/src/codex/catalog/account-models.ts +62 -0
- package/src/codex/catalog/aggregation.ts +14 -1
- package/src/codex/catalog/bundled.ts +282 -32
- package/src/codex/catalog/filesystem-evidence.ts +302 -0
- package/src/codex/catalog/metadata.ts +51 -6
- package/src/codex/catalog/parsing.ts +6 -3
- package/src/codex/catalog/provider-fetch.ts +576 -41
- package/src/codex/catalog/sync.ts +505 -66
- package/src/codex/catalog-admission.ts +197 -0
- package/src/codex/catalog-write-serialization.ts +241 -0
- package/src/codex/catalog.ts +2 -1
- package/src/codex/codex-write-lock.ts +372 -0
- package/src/codex/convergence-types.ts +593 -0
- package/src/codex/convergence.ts +441 -0
- package/src/codex/desired-state.ts +177 -0
- package/src/codex/features.ts +52 -8
- package/src/codex/generation.ts +202 -0
- package/src/codex/history-job.ts +257 -0
- package/src/codex/history-lock.ts +241 -0
- package/src/codex/history-migration-guardian.ts +18 -5
- package/src/codex/history-provider.ts +9 -2
- package/src/codex/history-transition.ts +105 -0
- package/src/codex/history-worker.ts +176 -0
- package/src/codex/inject-coordination.ts +245 -0
- package/src/codex/inject.ts +605 -124
- package/src/codex/integration-record.ts +266 -0
- package/src/codex/internal/catalog-writer.ts +203 -0
- package/src/codex/internal/history-writer.ts +80 -0
- package/src/codex/journal.ts +10 -1
- package/src/codex/main-account-cache.ts +24 -0
- package/src/codex/management-convergence.ts +114 -0
- package/src/codex/native-main-admission.ts +47 -0
- package/src/codex/native-main-auth-temp.ts +187 -0
- package/src/codex/native-main-claim.ts +167 -0
- package/src/codex/native-main-lock-file.ts +158 -0
- package/src/codex/native-main-owner.ts +315 -0
- package/src/codex/native-profile-api.ts +247 -0
- package/src/codex/native-profile-manager.ts +1512 -0
- package/src/codex/native-profile-processes.ts +121 -0
- package/src/codex/native-profile-recovery.ts +99 -0
- package/src/codex/native-profile-stage-store.ts +387 -0
- package/src/codex/native-profile-startup.ts +340 -0
- package/src/codex/native-profile-store.ts +855 -0
- package/src/codex/native-profile-types.ts +120 -0
- package/src/codex/native-residue.ts +557 -0
- package/src/codex/project-config-warnings.ts +18 -4
- package/src/codex/prompt-journal.ts +311 -0
- package/src/codex/prompt-layers.ts +967 -0
- package/src/codex/prompt-lock.ts +143 -0
- package/src/codex/quota-rejection.ts +224 -0
- package/src/codex/quota.ts +86 -3
- package/src/codex/routing.ts +299 -62
- package/src/codex/runtime.ts +159 -38
- package/src/codex/shim.ts +39 -13
- package/src/codex/subagent-model-fallback.ts +73 -12
- package/src/codex/transition-state.ts +604 -0
- package/src/codex/upstream-host-health.ts +70 -0
- package/src/codex/user-identity.ts +266 -0
- package/src/codex/write-coordination.ts +114 -0
- package/src/config.ts +562 -26
- package/src/generated/jawcode-model-metadata.ts +2 -2
- package/src/grok/inject.ts +15 -4
- package/src/grok/inspect.ts +45 -0
- package/src/images/loop.ts +113 -20
- package/src/integrations/config-io.ts +151 -0
- package/src/integrations/journal.ts +315 -0
- package/src/integrations/merge.ts +135 -0
- package/src/integrations/native/ownership-preflight.ts +165 -0
- package/src/integrations/ownership.ts +111 -0
- package/src/integrations/registry.ts +101 -0
- package/src/integrations/serialize.ts +235 -0
- package/src/integrations/state.ts +290 -0
- package/src/integrations/store.ts +103 -0
- package/src/integrations/writer.ts +492 -0
- package/src/lib/bounded-body.ts +46 -8
- package/src/lib/bun-runtime.ts +110 -1
- package/src/lib/bun-stream-caps.ts +2 -1
- package/src/lib/redact.ts +407 -2
- package/src/lib/shadow-call.ts +24 -0
- package/src/lib/translator-budget.ts +10 -0
- package/src/lib/upstream-reachability.ts +91 -0
- package/src/lib/upstream-retry.ts +154 -2
- package/src/lib/windows-secret-acl.ts +212 -11
- package/src/lib/winsw.ts +9 -3
- package/src/oauth/index.ts +61 -3
- package/src/oauth/key-providers.ts +4 -0
- package/src/oauth/kiro.ts +50 -6
- package/src/oauth/store.ts +31 -6
- package/src/oauth/token-guardian.ts +9 -3
- package/src/providers/codex-capacity.ts +288 -0
- package/src/providers/derive.ts +33 -1
- package/src/providers/free-directory.ts +3 -1
- package/src/providers/key-failover.ts +71 -3
- package/src/providers/openai-sidecar.ts +64 -4
- package/src/providers/openai-virtual-models.ts +1 -0
- package/src/providers/quota.ts +334 -26
- package/src/providers/registry.ts +284 -16
- package/src/providers/xai-transport.ts +11 -4
- package/src/responses/compaction.ts +8 -1
- package/src/responses/hosted-tool-policy.ts +9 -0
- package/src/responses/parser.ts +44 -2
- package/src/responses/reasoning-envelope.ts +9 -1
- package/src/responses/reasoning-replay-cache.ts +105 -0
- package/src/responses/spill-store.ts +45 -8
- package/src/responses/state.ts +161 -17
- package/src/router.ts +302 -16
- package/src/routing/analytics.ts +377 -0
- package/src/routing/capability.ts +204 -0
- package/src/routing/cost.ts +77 -0
- package/src/routing/evaluator.ts +444 -0
- package/src/routing/health.ts +401 -0
- package/src/routing/history/cursor.ts +43 -0
- package/src/routing/history/indexer.ts +590 -0
- package/src/routing/history/schema.ts +72 -0
- package/src/routing/profile.ts +423 -0
- package/src/routing/quota.ts +124 -0
- package/src/routing/request-evidence.ts +45 -0
- package/src/routing/trace.ts +686 -0
- package/src/server/auth-cors.ts +46 -6
- package/src/server/chat-completions.ts +28 -13
- package/src/server/claude-messages.ts +23 -15
- package/src/server/gui-static.ts +39 -10
- package/src/server/images.ts +10 -1
- package/src/server/index.ts +238 -52
- package/src/server/lifecycle.ts +228 -9
- package/src/server/live.ts +14 -3
- package/src/server/management/agent-settings-routes.ts +64 -14
- package/src/server/management/combo-routes.ts +5 -5
- package/src/server/management/config-routes.ts +1 -1
- package/src/server/management/context.ts +42 -2
- package/src/server/management/integration-routes.ts +538 -0
- package/src/server/management/logs-usage-routes.ts +1 -1
- package/src/server/management/model-routes.ts +32 -113
- package/src/server/management/model-rows.ts +117 -0
- package/src/server/management/native-integration-routes.ts +587 -0
- package/src/server/management/oauth-account-routes.ts +1 -1
- package/src/server/management/provider-routes.ts +218 -117
- package/src/server/management/request-history-routes.ts +191 -0
- package/src/server/management/routing-analytics-routes.ts +74 -0
- package/src/server/management/routing-profile-routes.ts +177 -0
- package/src/server/management/shared.ts +2 -2
- package/src/server/management/sidebar-routes.ts +47 -31
- package/src/server/management/sync-response.ts +69 -0
- package/src/server/management/system-restart.ts +276 -24
- package/src/server/management/system-routes.ts +4 -0
- package/src/server/management-api.ts +84 -9
- package/src/server/management-auth.ts +43 -5
- package/src/server/relay-eager.ts +82 -42
- package/src/server/relay.ts +120 -6
- package/src/server/request-log.ts +26 -6
- package/src/server/responses/collaboration.ts +63 -8
- package/src/server/responses/compact.ts +272 -41
- package/src/server/responses/core.ts +730 -132
- package/src/server/responses/fetch-helpers.ts +15 -1
- package/src/server/responses-item-id-repair.ts +32 -3
- package/src/server/responses-json-events.ts +52 -0
- package/src/server/responses-snapshot-repair.ts +621 -0
- package/src/server/search.ts +51 -6
- package/src/server/sse-payload-rewrite.ts +89 -12
- package/src/server/startup-health-cache.ts +7 -1
- package/src/server/ws-bridge.ts +11 -17
- package/src/service-manager-probe.ts +297 -0
- package/src/service.ts +222 -32
- package/src/tray/windows-tray.ps1 +9 -0
- package/src/tray/windows.ts +15 -7
- package/src/types.ts +194 -14
- package/src/update/index.ts +13 -13
- package/src/update/job.ts +24 -21
- package/src/update/notify.ts +7 -3
- package/src/usage/cost.ts +0 -0
- package/src/usage/expected-prices.ts +129 -10
- package/src/usage/log.ts +50 -15
- package/src/usage/summary.ts +4 -4
- package/src/vision/index.ts +6 -1
- package/src/web-search/loop.ts +161 -34
- package/gui/dist/assets/index-OY43ubAq.css +0 -1
- package/gui/dist/assets/index-YwNnKZcL.js +0 -67
- package/gui/dist/provider-icons/antigravity.svg +0 -1
- package/gui/dist/provider-icons/claude.svg +0 -1
- package/gui/dist/provider-icons/copilot.svg +0 -1
- package/gui/dist/provider-icons/cursor.svg +0 -2
- package/gui/dist/provider-icons/gemini.svg +0 -1
- package/gui/dist/provider-icons/grok-color.svg +0 -1
- package/gui/dist/provider-icons/kiro.svg +0 -14
package/src/oauth/index.ts
CHANGED
|
@@ -4,7 +4,7 @@ import type { OcxConfig, OcxProviderConfig, RefreshPolicy } from "../types";
|
|
|
4
4
|
import { loadConfig, resolveEnvValue, saveConfig } from "../config";
|
|
5
5
|
import { maskEmail } from "../lib/privacy";
|
|
6
6
|
import { KiroTokenRefreshError, environmentKiroRoutingMetadata, loginKiro, refreshKiroToken, settleKiroLoginTransaction } from "./kiro";
|
|
7
|
-
import { getAccountCredential, getAccountSet, removeAccount, saveAccountCredential, saveCredential, setActiveAccount, getCredential, credentialGeneration, createOAuthRefreshIntentLock, mergeAccountCredential, markAccountNeedsReauthIfGeneration, readOAuthRefreshIntent, writeOAuthRefreshIntent, markOAuthRefreshIntentStaleOwner, clearOAuthRefreshIntent, OAuthMutationBusyError } from "./store";
|
|
7
|
+
import { getAccountCredential, getAccountSet, removeAccount, saveAccountCredential, saveCredential, setActiveAccount, getCredential, credentialGeneration, createOAuthRefreshIntentLock, mergeAccountCredential, markAccountNeedsReauthIfGeneration, readOAuthRefreshIntent, writeOAuthRefreshIntent, markOAuthRefreshIntentStaleOwner, clearOAuthRefreshIntent, normalizeAuthStoreBuffer, OAuthMutationBusyError } from "./store";
|
|
8
8
|
import { loginXai, refreshXaiToken, XAI_LOCAL_CLI_DETACH_WARNING, XaiTokenRequestError } from "./xai";
|
|
9
9
|
import { ANTHROPIC_OAUTH_BETA, AnthropicTokenError, loginAnthropic, refreshAnthropicToken } from "./anthropic";
|
|
10
10
|
import { loginKimi, refreshKimiToken } from "./kimi";
|
|
@@ -56,6 +56,20 @@ export interface OAuthAccessSnapshot {
|
|
|
56
56
|
kiro?: Pick<KiroOAuthMetadata, "profileArn" | "apiRegion" | "ssoRegion">;
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
export interface ObservedOAuthAccessSnapshot extends OAuthAccessSnapshot {
|
|
60
|
+
/** Allowlisted provider API origin consumed by GitHub Copilot model discovery. */
|
|
61
|
+
apiBaseUrl?: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export type OAuthActiveTokenObservation =
|
|
65
|
+
| { readonly kind: "available"; readonly snapshot: ObservedOAuthAccessSnapshot }
|
|
66
|
+
| { readonly kind: "missing" }
|
|
67
|
+
| { readonly kind: "malformed" }
|
|
68
|
+
| { readonly kind: "needs-reauth" }
|
|
69
|
+
| { readonly kind: "expired" }
|
|
70
|
+
| { readonly kind: "near-expiry" }
|
|
71
|
+
| { readonly kind: "unsupported" };
|
|
72
|
+
|
|
59
73
|
const MAX_OAUTH_TOKEN_REFRESH_FLIGHTS = 32;
|
|
60
74
|
const OAUTH_TOKEN_REFRESH_FLIGHT_STALE_MS = 120_000;
|
|
61
75
|
interface OAuthRefreshFlightEvidence { flightId: string; dispatched: boolean }
|
|
@@ -278,6 +292,38 @@ function accessSnapshot(provider: string, accountId: string, cred: OAuthCredenti
|
|
|
278
292
|
};
|
|
279
293
|
}
|
|
280
294
|
|
|
295
|
+
/**
|
|
296
|
+
* Observe the active OAuth token from an auth-store buffer supplied by its filesystem owner.
|
|
297
|
+
* Missing, malformed, reauth-required, and expiring credentials are typed no-token outcomes;
|
|
298
|
+
* this path never refreshes, locks, hardens, backs up, or persists credentials.
|
|
299
|
+
*/
|
|
300
|
+
export function observeActiveOAuthAccessToken(
|
|
301
|
+
provider: string,
|
|
302
|
+
authStoreBuffer: Uint8Array | null,
|
|
303
|
+
now = Date.now(),
|
|
304
|
+
): OAuthActiveTokenObservation {
|
|
305
|
+
const authStore = normalizeAuthStoreBuffer(authStoreBuffer);
|
|
306
|
+
if (authStore.kind === "absent") return { kind: "missing" };
|
|
307
|
+
if (authStore.kind === "malformed") return { kind: "malformed" };
|
|
308
|
+
if (!isOAuthProvider(provider)) return { kind: "unsupported" };
|
|
309
|
+
|
|
310
|
+
const accountSet = authStore.store[provider];
|
|
311
|
+
const account = accountSet?.accounts.find(candidate => candidate.id === accountSet.activeAccountId);
|
|
312
|
+
if (!account) return { kind: "missing" };
|
|
313
|
+
if (account.needsReauth) return { kind: "needs-reauth" };
|
|
314
|
+
if (account.credential.expires <= now) return { kind: "expired" };
|
|
315
|
+
if (account.credential.expires <= now + REFRESH_SKEW_MS) return { kind: "near-expiry" };
|
|
316
|
+
|
|
317
|
+
const apiBaseUrl = validateCopilotApiBaseUrl(account.credential.apiBaseUrl);
|
|
318
|
+
return {
|
|
319
|
+
kind: "available",
|
|
320
|
+
snapshot: {
|
|
321
|
+
...accessSnapshot(provider, account.id, account.credential),
|
|
322
|
+
...(apiBaseUrl ? { apiBaseUrl } : {}),
|
|
323
|
+
},
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
|
|
281
327
|
async function resolveAccessSnapshotForAccount(
|
|
282
328
|
provider: string,
|
|
283
329
|
accountId: string,
|
|
@@ -583,13 +629,25 @@ function modelDiscoveryTransportSeed(providerName: string, prov: OcxProviderConf
|
|
|
583
629
|
* Everyone else uses the OpenAI-style `/models` + Bearer with a `{ data: [{ id, owned_by? }] }`
|
|
584
630
|
* response.
|
|
585
631
|
*/
|
|
586
|
-
export
|
|
632
|
+
export interface ModelsRequestObservedAuth {
|
|
633
|
+
readonly oauthApiBaseUrl?: string;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
export function buildModelsRequest(
|
|
637
|
+
prov: OcxProviderConfig,
|
|
638
|
+
apiKey: string | undefined,
|
|
639
|
+
providerName = "",
|
|
640
|
+
observedAuth?: ModelsRequestObservedAuth,
|
|
641
|
+
): { url: string; headers: Record<string, string> } {
|
|
587
642
|
const transportSeed = modelDiscoveryTransportSeed(providerName, prov);
|
|
643
|
+
const copilotApiBaseUrl = observedAuth === undefined
|
|
644
|
+
? (providerName === "github-copilot" ? getOAuthCredentialApiBaseUrl(providerName) : undefined)
|
|
645
|
+
: observedAuth.oauthApiBaseUrl;
|
|
588
646
|
const effectiveProvider = resolveProviderTransport(
|
|
589
647
|
providerName,
|
|
590
648
|
transportSeed,
|
|
591
649
|
undefined,
|
|
592
|
-
|
|
650
|
+
copilotApiBaseUrl,
|
|
593
651
|
);
|
|
594
652
|
const headers: Record<string, string> = { ...(effectiveProvider.headers ?? {}) };
|
|
595
653
|
const discoveryUrl = (defaultUrl: string): string => resolveProviderModelDiscoveryUrl(
|
|
@@ -52,6 +52,10 @@ export async function validateApiKey(
|
|
|
52
52
|
key: string,
|
|
53
53
|
): Promise<boolean | "unknown"> {
|
|
54
54
|
try {
|
|
55
|
+
// A public model catalog cannot prove that the supplied key is valid. Returning unknown keeps
|
|
56
|
+
// the best-effort login flow available without persisting a false-positive validation result.
|
|
57
|
+
if (provider.apiKeyValidation === "unknown") return "unknown";
|
|
58
|
+
|
|
55
59
|
if (provider.adapter === "anthropic") {
|
|
56
60
|
const base = provider.baseUrl.replace(/\/v1\/?$/, "");
|
|
57
61
|
const res = await fetch(`${base}/v1/messages`, {
|
package/src/oauth/kiro.ts
CHANGED
|
@@ -172,13 +172,37 @@ async function defaultKiroCliRunner(args: string[], signal?: AbortSignal): Promi
|
|
|
172
172
|
}
|
|
173
173
|
}
|
|
174
174
|
|
|
175
|
-
|
|
175
|
+
/** Kiro profile ARN structure: arn:<partition>:codewhisperer:<region>:<account>:profile/<id> */
|
|
176
|
+
const KIRO_PROFILE_ARN_PATTERN = /^arn:[a-z0-9-]+:codewhisperer:[a-z0-9-]+:\d{12}:profile\/[A-Za-z0-9-]+$/;
|
|
177
|
+
const KIRO_PROFILE_ARN_MAX_LENGTH = 256;
|
|
178
|
+
|
|
179
|
+
function parseKiroProfileArn(value: unknown): string | undefined {
|
|
180
|
+
if (typeof value !== "string") return undefined;
|
|
181
|
+
const trimmed = value.trim();
|
|
182
|
+
if (trimmed.length === 0 || trimmed.length > KIRO_PROFILE_ARN_MAX_LENGTH) return undefined;
|
|
183
|
+
return KIRO_PROFILE_ARN_PATTERN.test(trimmed) ? trimmed : undefined;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function profileArnFromWhoami(parsed: Record<string, unknown>): string | undefined {
|
|
187
|
+
// Only narrowly-named documented-ish shapes; never invent an ARN (#993).
|
|
188
|
+
return parseKiroProfileArn(parsed.profileArn)
|
|
189
|
+
?? parseKiroProfileArn(parsed.profile_arn)
|
|
190
|
+
?? (parsed.profile && typeof parsed.profile === "object" && !Array.isArray(parsed.profile)
|
|
191
|
+
? parseKiroProfileArn((parsed.profile as Record<string, unknown>).arn)
|
|
192
|
+
: undefined);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async function readKiroCliIdentity(runner: KiroCliRunner, signal?: AbortSignal): Promise<{ email?: string; profileArn?: string }> {
|
|
176
196
|
try {
|
|
177
197
|
const result = await runner(["whoami", "--format", "json"], signal);
|
|
178
198
|
if (result.exitCode !== 0) return {};
|
|
179
|
-
const parsed = JSON.parse(result.stdout) as
|
|
199
|
+
const parsed = JSON.parse(result.stdout) as Record<string, unknown>;
|
|
180
200
|
const email = typeof parsed.email === "string" ? parsed.email.trim().toLowerCase() : "";
|
|
181
|
-
|
|
201
|
+
const profileArn = profileArnFromWhoami(parsed);
|
|
202
|
+
return {
|
|
203
|
+
...(email && email.length <= 320 ? { email } : {}),
|
|
204
|
+
...(profileArn ? { profileArn } : {}),
|
|
205
|
+
};
|
|
182
206
|
} catch {
|
|
183
207
|
return {};
|
|
184
208
|
}
|
|
@@ -251,14 +275,34 @@ async function oauthCredentialFromImported(
|
|
|
251
275
|
runner: KiroCliRunner,
|
|
252
276
|
signal?: AbortSignal,
|
|
253
277
|
): Promise<OAuthCredentials> {
|
|
254
|
-
|
|
255
|
-
|
|
278
|
+
let identity: { email?: string; profileArn?: string } = {};
|
|
279
|
+
if (imported.source === "sqlite") {
|
|
280
|
+
identity = await readKiroCliIdentity(runner, signal);
|
|
281
|
+
// Session-switch race (#993 review): another process may have switched the
|
|
282
|
+
// active Kiro CLI session between the SQLite read and whoami. Accept
|
|
283
|
+
// whoami's identity only when the session token STILL matches the import —
|
|
284
|
+
// refresh token, or access token when refresh is absent.
|
|
285
|
+
if (identity.profileArn !== undefined) {
|
|
286
|
+
const current = readKiroCliSqliteCredential();
|
|
287
|
+
const importedKey = imported.refresh || imported.access;
|
|
288
|
+
const currentKey = current ? current.refresh || current.access : "";
|
|
289
|
+
if (!current || currentKey !== importedKey) identity = {};
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
// Builder ID imports often lack a profileArn in SQLite; whoami against the
|
|
293
|
+
// SAME active CLI session can supply it (#993). Imported stays authoritative.
|
|
294
|
+
const resolvedProfileArn = imported.profileArn ?? identity.profileArn;
|
|
295
|
+
const metadata: KiroOAuthMetadata | undefined = (() => {
|
|
296
|
+
const base = metadataFromImported(imported) ?? {};
|
|
297
|
+
if (resolvedProfileArn && !base.profileArn) base.profileArn = resolvedProfileArn;
|
|
298
|
+
return Object.keys(base).length > 0 ? base : undefined;
|
|
299
|
+
})();
|
|
256
300
|
return {
|
|
257
301
|
access: imported.access,
|
|
258
302
|
refresh: imported.refresh,
|
|
259
303
|
expires: imported.expires,
|
|
260
304
|
source: imported.source === "json" ? "credential-file" : "local-cli",
|
|
261
|
-
...(
|
|
305
|
+
...(resolvedProfileArn ? { accountId: resolvedProfileArn } : {}),
|
|
262
306
|
...(identity.email ? { email: identity.email } : {}),
|
|
263
307
|
...(metadata ? { kiro: metadata } : {}),
|
|
264
308
|
};
|
package/src/oauth/store.ts
CHANGED
|
@@ -29,7 +29,14 @@ import {
|
|
|
29
29
|
import { validateCopilotApiBaseUrl } from "./github-copilot";
|
|
30
30
|
import type { OAuthCredentialSource, OAuthCredentials, ProviderAccount, ProviderAccountSet } from "./types";
|
|
31
31
|
|
|
32
|
-
type AuthStore = Record<string, ProviderAccountSet>;
|
|
32
|
+
export type AuthStore = Record<string, ProviderAccountSet>;
|
|
33
|
+
|
|
34
|
+
export type AuthStoreBufferSnapshot =
|
|
35
|
+
| { readonly kind: "ready"; readonly store: AuthStore }
|
|
36
|
+
| { readonly kind: "absent" }
|
|
37
|
+
| { readonly kind: "malformed" };
|
|
38
|
+
|
|
39
|
+
const authStoreDecoder = new TextDecoder("utf-8", { fatal: true });
|
|
33
40
|
let lastReconciledGeneration = 0;
|
|
34
41
|
let liveOAuthAccountKeys = new Set<string>();
|
|
35
42
|
|
|
@@ -142,6 +149,27 @@ export function loadAuthStore(): AuthStore {
|
|
|
142
149
|
return loadAuthStoreInternal().store;
|
|
143
150
|
}
|
|
144
151
|
|
|
152
|
+
/**
|
|
153
|
+
* Pure normalization for auth-store bytes already read by another owner.
|
|
154
|
+
* This function performs no filesystem consultation, hardening, backup, or persistence.
|
|
155
|
+
*/
|
|
156
|
+
export function normalizeAuthStoreBuffer(buffer: Uint8Array | null): AuthStoreBufferSnapshot {
|
|
157
|
+
if (buffer === null) return { kind: "absent" };
|
|
158
|
+
try {
|
|
159
|
+
const parsed: unknown = JSON.parse(authStoreDecoder.decode(buffer));
|
|
160
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
161
|
+
return { kind: "malformed" };
|
|
162
|
+
}
|
|
163
|
+
const { store } = normalizeAuthStore(parsed);
|
|
164
|
+
if (Object.keys(parsed).length > 0 && Object.keys(store).length === 0) {
|
|
165
|
+
return { kind: "malformed" };
|
|
166
|
+
}
|
|
167
|
+
return { kind: "ready", store };
|
|
168
|
+
} catch {
|
|
169
|
+
return { kind: "malformed" };
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
145
173
|
/**
|
|
146
174
|
* Observe-only auth store read for diagnostics (`ocx doctor` / status).
|
|
147
175
|
* Does not chmod paths or backup invalid JSON — corrupt files are treated as empty.
|
|
@@ -149,11 +177,8 @@ export function loadAuthStore(): AuthStore {
|
|
|
149
177
|
export function peekAuthStore(): AuthStore {
|
|
150
178
|
const path = getAuthStorePath();
|
|
151
179
|
if (!existsSync(path)) return {};
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
} catch {
|
|
155
|
-
return {};
|
|
156
|
-
}
|
|
180
|
+
const snapshot = normalizeAuthStoreBuffer(readFileSync(path));
|
|
181
|
+
return snapshot.kind === "ready" ? snapshot.store : {};
|
|
157
182
|
}
|
|
158
183
|
|
|
159
184
|
function persist(store: AuthStore): void {
|
|
@@ -30,6 +30,7 @@ import { getMainAccountToken, MAIN_CODEX_ACCOUNT_ID } from "../codex/main-accoun
|
|
|
30
30
|
import { isCanonicalOpenAiForwardProvider, OPENAI_CODEX_PROVIDER_ID } from "../providers/openai-tiers";
|
|
31
31
|
import { providerCodexAccountMode } from "../providers/registry";
|
|
32
32
|
import { captureConfigGeneration, type GenerationContext } from "../lib/state-store-sweeper";
|
|
33
|
+
import { tryAcquireNativeMainProfileClaim } from "../codex/native-main-admission";
|
|
33
34
|
|
|
34
35
|
export interface TokenGuardianHandle {
|
|
35
36
|
stop(): void;
|
|
@@ -175,11 +176,14 @@ export async function guardianSweep(nowMs: number = Date.now()): Promise<Guardia
|
|
|
175
176
|
) {
|
|
176
177
|
const mode = providerCodexAccountMode(OPENAI_CODEX_PROVIDER_ID, openai) ?? "pool";
|
|
177
178
|
if (opts.codexWarmupEnabled) {
|
|
178
|
-
const mainToken = getMainAccountToken();
|
|
179
179
|
const key = `codex:${MAIN_CODEX_ACCOUNT_ID}`;
|
|
180
|
-
if (
|
|
180
|
+
if (!inBackoff(key, nowMs)) {
|
|
181
181
|
tasks.push(async () => {
|
|
182
|
+
const nativeMainLease = tryAcquireNativeMainProfileClaim();
|
|
183
|
+
if (!nativeMainLease) return;
|
|
182
184
|
try {
|
|
185
|
+
const mainToken = getMainAccountToken();
|
|
186
|
+
if (!mainToken) return;
|
|
183
187
|
await warmCodexAccount({
|
|
184
188
|
accessToken: mainToken.accessToken,
|
|
185
189
|
chatgptAccountId: mainToken.chatgptAccountId,
|
|
@@ -190,9 +194,11 @@ export async function guardianSweep(nowMs: number = Date.now()): Promise<Guardia
|
|
|
190
194
|
} catch (err) {
|
|
191
195
|
recordFailure(key, nowMs, opts.backoffBaseSeconds, opts.backoffMaxSeconds, false, writerGeneration);
|
|
192
196
|
result.failed.push(key);
|
|
197
|
+
} finally {
|
|
198
|
+
nativeMainLease.release();
|
|
193
199
|
}
|
|
194
200
|
});
|
|
195
|
-
} else
|
|
201
|
+
} else {
|
|
196
202
|
result.skippedBackoff.push(key);
|
|
197
203
|
}
|
|
198
204
|
}
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
export const CODEX_CONFIGURED_CAPACITY_WEIGHTS = {
|
|
2
|
+
plus: 1,
|
|
3
|
+
business: 1,
|
|
4
|
+
prolite: 5,
|
|
5
|
+
pro: 20,
|
|
6
|
+
} as const;
|
|
7
|
+
|
|
8
|
+
/** Match the provider-report last-good freshness bound. */
|
|
9
|
+
export const CODEX_CAPACITY_MAX_QUOTA_AGE_MS = 30 * 60_000;
|
|
10
|
+
|
|
11
|
+
export type CodexCapacityQuota = {
|
|
12
|
+
fiveHourPercent?: number;
|
|
13
|
+
fiveHourResetAt?: number;
|
|
14
|
+
weeklyPercent?: number;
|
|
15
|
+
weeklyResetAt?: number;
|
|
16
|
+
monthlyPercent?: number;
|
|
17
|
+
monthlyResetAt?: number;
|
|
18
|
+
customWindows?: Array<{ label: string; percent: number; resetAt?: number }>;
|
|
19
|
+
updatedAt: number;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export interface CodexCapacityAccount {
|
|
23
|
+
isMain: boolean;
|
|
24
|
+
active?: boolean;
|
|
25
|
+
plan?: string | null;
|
|
26
|
+
paused: boolean;
|
|
27
|
+
needsReauth?: boolean;
|
|
28
|
+
quota: CodexCapacityQuota | null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface CodexCapacityWindowAggregation {
|
|
32
|
+
usedPercent: number;
|
|
33
|
+
includedAccounts: number;
|
|
34
|
+
excludedAccounts: number;
|
|
35
|
+
incomplete: boolean;
|
|
36
|
+
/** Internal calculation evidence; stripped from the management API response. */
|
|
37
|
+
totalWeight?: number;
|
|
38
|
+
/** Internal calculation evidence; stripped from the management API response. */
|
|
39
|
+
consumedWeight?: number;
|
|
40
|
+
/** Internal calculation evidence; stripped from the management API response. */
|
|
41
|
+
remainingWeight?: number;
|
|
42
|
+
updatedAt: number;
|
|
43
|
+
nextRecoveryAt?: number;
|
|
44
|
+
nextRecoveryPercent?: number;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface CodexCapacityAggregation {
|
|
48
|
+
kind: "capacity-weighted-v1";
|
|
49
|
+
scope: "routable-known";
|
|
50
|
+
includedAccounts: number;
|
|
51
|
+
excludedAccounts: number;
|
|
52
|
+
unknownPlanAccounts: number;
|
|
53
|
+
missingQuotaAccounts: number;
|
|
54
|
+
pausedAccounts: number;
|
|
55
|
+
reauthAccounts: number;
|
|
56
|
+
staleQuotaAccounts: number;
|
|
57
|
+
partialWindowAccounts: number;
|
|
58
|
+
incomplete: boolean;
|
|
59
|
+
presentation?: "aggregate" | "effective-account-fallback" | "coverage-only";
|
|
60
|
+
fiveHour?: CodexCapacityWindowAggregation;
|
|
61
|
+
weekly?: CodexCapacityWindowAggregation;
|
|
62
|
+
monthly?: CodexCapacityWindowAggregation;
|
|
63
|
+
customWindows?: Array<CodexCapacityWindowAggregation & { label: string }>;
|
|
64
|
+
currentAccount?: {
|
|
65
|
+
isMain: boolean;
|
|
66
|
+
plan?: string | null;
|
|
67
|
+
quota: CodexCapacityQuota | null;
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface CodexCapacityResult {
|
|
72
|
+
quota: CodexCapacityQuota | null;
|
|
73
|
+
aggregation: CodexCapacityAggregation | null;
|
|
74
|
+
currentAccount?: CodexCapacityAggregation["currentAccount"];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
type MutableWindow = {
|
|
78
|
+
totalWeight: number;
|
|
79
|
+
consumedWeight: number;
|
|
80
|
+
includedAccounts: number;
|
|
81
|
+
recoveries: Map<number, number>;
|
|
82
|
+
oldestUpdatedAt: number;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
function configuredWeight(plan: string | null | undefined): number | undefined {
|
|
86
|
+
const normalized = plan?.trim().toLowerCase();
|
|
87
|
+
return normalized && Object.hasOwn(CODEX_CONFIGURED_CAPACITY_WEIGHTS, normalized)
|
|
88
|
+
? CODEX_CONFIGURED_CAPACITY_WEIGHTS[normalized as keyof typeof CODEX_CONFIGURED_CAPACITY_WEIGHTS]
|
|
89
|
+
: undefined;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function normalizedPercent(value: unknown): number | undefined {
|
|
93
|
+
return typeof value === "number" && Number.isFinite(value)
|
|
94
|
+
? Math.max(0, Math.min(100, value))
|
|
95
|
+
: undefined;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function futureResetMs(value: unknown, now: number): number | undefined {
|
|
99
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return undefined;
|
|
100
|
+
const milliseconds = value > 10_000_000_000 ? value : value * 1000;
|
|
101
|
+
return milliseconds > now ? milliseconds : undefined;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function hasKnownQuotaWindow(quota: CodexCapacityQuota | null): quota is CodexCapacityQuota {
|
|
105
|
+
if (!quota) return false;
|
|
106
|
+
return normalizedPercent(quota.fiveHourPercent) !== undefined
|
|
107
|
+
|| normalizedPercent(quota.weeklyPercent) !== undefined
|
|
108
|
+
|| normalizedPercent(quota.monthlyPercent) !== undefined
|
|
109
|
+
|| !!quota.customWindows?.some(window => normalizedPercent(window.percent) !== undefined);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function currentQuotaForDisplay(account: CodexCapacityAccount, now: number): CodexCapacityQuota | null {
|
|
113
|
+
const quota = account.quota;
|
|
114
|
+
const fresh = !!quota
|
|
115
|
+
&& Number.isFinite(quota.updatedAt)
|
|
116
|
+
&& now - quota.updatedAt <= CODEX_CAPACITY_MAX_QUOTA_AGE_MS;
|
|
117
|
+
return !account.paused && !account.needsReauth && fresh && hasKnownQuotaWindow(quota) ? quota : null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function addWindow(
|
|
121
|
+
windows: Map<string, MutableWindow>,
|
|
122
|
+
key: string,
|
|
123
|
+
weight: number,
|
|
124
|
+
percent: number,
|
|
125
|
+
resetAt: number | undefined,
|
|
126
|
+
updatedAt: number,
|
|
127
|
+
): void {
|
|
128
|
+
const window = windows.get(key) ?? {
|
|
129
|
+
totalWeight: 0,
|
|
130
|
+
consumedWeight: 0,
|
|
131
|
+
includedAccounts: 0,
|
|
132
|
+
recoveries: new Map<number, number>(),
|
|
133
|
+
oldestUpdatedAt: updatedAt,
|
|
134
|
+
};
|
|
135
|
+
const consumed = weight * percent / 100;
|
|
136
|
+
window.totalWeight += weight;
|
|
137
|
+
window.consumedWeight += consumed;
|
|
138
|
+
window.includedAccounts += 1;
|
|
139
|
+
window.oldestUpdatedAt = Math.min(window.oldestUpdatedAt, updatedAt);
|
|
140
|
+
if (resetAt !== undefined && consumed > 0) {
|
|
141
|
+
window.recoveries.set(resetAt, (window.recoveries.get(resetAt) ?? 0) + consumed);
|
|
142
|
+
}
|
|
143
|
+
windows.set(key, window);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function finalizeWindow(window: MutableWindow, totalAccounts: number): CodexCapacityWindowAggregation {
|
|
147
|
+
const nextRecoveryAt = [...window.recoveries.keys()].sort((a, b) => a - b)[0];
|
|
148
|
+
const recovered = nextRecoveryAt === undefined ? undefined : window.recoveries.get(nextRecoveryAt);
|
|
149
|
+
return {
|
|
150
|
+
usedPercent: window.consumedWeight / window.totalWeight * 100,
|
|
151
|
+
includedAccounts: window.includedAccounts,
|
|
152
|
+
excludedAccounts: totalAccounts - window.includedAccounts,
|
|
153
|
+
incomplete: window.includedAccounts < totalAccounts,
|
|
154
|
+
totalWeight: window.totalWeight,
|
|
155
|
+
consumedWeight: window.consumedWeight,
|
|
156
|
+
remainingWeight: window.totalWeight - window.consumedWeight,
|
|
157
|
+
updatedAt: window.oldestUpdatedAt,
|
|
158
|
+
...(nextRecoveryAt !== undefined ? { nextRecoveryAt } : {}),
|
|
159
|
+
...(recovered !== undefined ? { nextRecoveryPercent: recovered / window.totalWeight * 100 } : {}),
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Display-only configured-weight estimate. It never participates in account selection or routing. */
|
|
164
|
+
export function aggregateCodexPoolCapacity(
|
|
165
|
+
accounts: readonly CodexCapacityAccount[],
|
|
166
|
+
now = Date.now(),
|
|
167
|
+
): CodexCapacityResult {
|
|
168
|
+
const current = accounts.find(account => account.active)
|
|
169
|
+
?? accounts.find(account => account.isMain)
|
|
170
|
+
?? accounts[0];
|
|
171
|
+
const currentAccount = current ? {
|
|
172
|
+
isMain: current.isMain,
|
|
173
|
+
...(current.plan !== undefined ? { plan: current.plan } : {}),
|
|
174
|
+
quota: currentQuotaForDisplay(current, now),
|
|
175
|
+
} : undefined;
|
|
176
|
+
const windows = new Map<string, MutableWindow>();
|
|
177
|
+
const included = new Set<CodexCapacityAccount>();
|
|
178
|
+
const contributions = new Map<CodexCapacityAccount, Set<string>>();
|
|
179
|
+
let unknownPlanAccounts = 0;
|
|
180
|
+
let missingQuotaAccounts = 0;
|
|
181
|
+
let pausedAccounts = 0;
|
|
182
|
+
let reauthAccounts = 0;
|
|
183
|
+
let staleQuotaAccounts = 0;
|
|
184
|
+
|
|
185
|
+
for (const account of accounts) {
|
|
186
|
+
const weight = configuredWeight(account.plan);
|
|
187
|
+
if (weight === undefined) unknownPlanAccounts += 1;
|
|
188
|
+
if (account.paused) pausedAccounts += 1;
|
|
189
|
+
if (account.needsReauth) reauthAccounts += 1;
|
|
190
|
+
const quota = account.quota;
|
|
191
|
+
const quotaFresh = !!quota
|
|
192
|
+
&& Number.isFinite(quota.updatedAt)
|
|
193
|
+
&& now - quota.updatedAt <= CODEX_CAPACITY_MAX_QUOTA_AGE_MS;
|
|
194
|
+
if (quota && !quotaFresh) staleQuotaAccounts += 1;
|
|
195
|
+
const standard = quota ? [
|
|
196
|
+
["fiveHour", quota.fiveHourPercent, quota.fiveHourResetAt],
|
|
197
|
+
["weekly", quota.weeklyPercent, quota.weeklyResetAt],
|
|
198
|
+
["monthly", quota.monthlyPercent, quota.monthlyResetAt],
|
|
199
|
+
] as const : [];
|
|
200
|
+
const custom = quota?.customWindows ?? [];
|
|
201
|
+
const hasQuota = hasKnownQuotaWindow(quota);
|
|
202
|
+
if (!hasQuota) missingQuotaAccounts += 1;
|
|
203
|
+
if (account.paused || account.needsReauth || weight === undefined || !quota || !hasQuota || !quotaFresh) continue;
|
|
204
|
+
|
|
205
|
+
let contributed = false;
|
|
206
|
+
const contributionKeys = new Set<string>();
|
|
207
|
+
for (const [key, rawPercent, rawReset] of standard) {
|
|
208
|
+
const percent = normalizedPercent(rawPercent);
|
|
209
|
+
if (percent === undefined) continue;
|
|
210
|
+
addWindow(windows, key, weight, percent, futureResetMs(rawReset, now), quota.updatedAt);
|
|
211
|
+
contributionKeys.add(key);
|
|
212
|
+
contributed = true;
|
|
213
|
+
}
|
|
214
|
+
for (const customWindow of custom) {
|
|
215
|
+
const percent = normalizedPercent(customWindow.percent);
|
|
216
|
+
if (percent === undefined) continue;
|
|
217
|
+
addWindow(windows, `custom:${customWindow.label}`, weight, percent, futureResetMs(customWindow.resetAt, now), quota.updatedAt);
|
|
218
|
+
contributionKeys.add(`custom:${customWindow.label}`);
|
|
219
|
+
contributed = true;
|
|
220
|
+
}
|
|
221
|
+
if (contributed) {
|
|
222
|
+
included.add(account);
|
|
223
|
+
contributions.set(account, contributionKeys);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (windows.size === 0) {
|
|
228
|
+
if (accounts.length === 0) return { quota: null, aggregation: null, ...(currentAccount ? { currentAccount } : {}) };
|
|
229
|
+
const aggregation: CodexCapacityAggregation = {
|
|
230
|
+
kind: "capacity-weighted-v1",
|
|
231
|
+
scope: "routable-known",
|
|
232
|
+
includedAccounts: 0,
|
|
233
|
+
excludedAccounts: accounts.length,
|
|
234
|
+
unknownPlanAccounts,
|
|
235
|
+
missingQuotaAccounts,
|
|
236
|
+
pausedAccounts,
|
|
237
|
+
reauthAccounts,
|
|
238
|
+
staleQuotaAccounts,
|
|
239
|
+
partialWindowAccounts: 0,
|
|
240
|
+
incomplete: true,
|
|
241
|
+
...(currentAccount ? { currentAccount } : {}),
|
|
242
|
+
};
|
|
243
|
+
return { quota: null, aggregation, ...(currentAccount ? { currentAccount } : {}) };
|
|
244
|
+
}
|
|
245
|
+
const fiveHour = windows.get("fiveHour") ? finalizeWindow(windows.get("fiveHour")!, accounts.length) : undefined;
|
|
246
|
+
const weekly = windows.get("weekly") ? finalizeWindow(windows.get("weekly")!, accounts.length) : undefined;
|
|
247
|
+
const monthly = windows.get("monthly") ? finalizeWindow(windows.get("monthly")!, accounts.length) : undefined;
|
|
248
|
+
const customWindows = [...windows.entries()].flatMap(([key, window]) => key.startsWith("custom:")
|
|
249
|
+
? [{ label: key.slice("custom:".length), ...finalizeWindow(window, accounts.length) }]
|
|
250
|
+
: []);
|
|
251
|
+
const quota: CodexCapacityQuota = {
|
|
252
|
+
...(fiveHour ? { fiveHourPercent: fiveHour.usedPercent } : {}),
|
|
253
|
+
...(weekly ? { weeklyPercent: weekly.usedPercent } : {}),
|
|
254
|
+
...(monthly ? { monthlyPercent: monthly.usedPercent } : {}),
|
|
255
|
+
...(customWindows.length > 0 ? {
|
|
256
|
+
customWindows: customWindows.map(window => ({ label: window.label, percent: window.usedPercent })),
|
|
257
|
+
} : {}),
|
|
258
|
+
updatedAt: Math.min(
|
|
259
|
+
...[fiveHour, weekly, monthly, ...customWindows]
|
|
260
|
+
.flatMap(window => window ? [window.updatedAt] : []),
|
|
261
|
+
),
|
|
262
|
+
};
|
|
263
|
+
const visibleWindowKeys = [...windows.keys()];
|
|
264
|
+
const partialWindowAccounts = accounts.filter(account => {
|
|
265
|
+
const keys = contributions.get(account);
|
|
266
|
+
return !!keys && visibleWindowKeys.some(key => !keys.has(key));
|
|
267
|
+
}).length;
|
|
268
|
+
const excludedAccounts = accounts.length - included.size;
|
|
269
|
+
const aggregation: CodexCapacityAggregation = {
|
|
270
|
+
kind: "capacity-weighted-v1",
|
|
271
|
+
scope: "routable-known",
|
|
272
|
+
includedAccounts: included.size,
|
|
273
|
+
excludedAccounts,
|
|
274
|
+
unknownPlanAccounts,
|
|
275
|
+
missingQuotaAccounts,
|
|
276
|
+
pausedAccounts,
|
|
277
|
+
reauthAccounts,
|
|
278
|
+
staleQuotaAccounts,
|
|
279
|
+
partialWindowAccounts,
|
|
280
|
+
incomplete: excludedAccounts > 0 || partialWindowAccounts > 0,
|
|
281
|
+
...(fiveHour ? { fiveHour } : {}),
|
|
282
|
+
...(weekly ? { weekly } : {}),
|
|
283
|
+
...(monthly ? { monthly } : {}),
|
|
284
|
+
...(customWindows.length > 0 ? { customWindows } : {}),
|
|
285
|
+
...(currentAccount ? { currentAccount } : {}),
|
|
286
|
+
};
|
|
287
|
+
return { quota, aggregation, ...(currentAccount ? { currentAccount } : {}) };
|
|
288
|
+
}
|
package/src/providers/derive.ts
CHANGED
|
@@ -6,6 +6,7 @@ export interface DerivedKeyLoginProvider {
|
|
|
6
6
|
baseUrl: string;
|
|
7
7
|
responsesPath?: string;
|
|
8
8
|
adapter: string;
|
|
9
|
+
apiKeyValidation?: "unknown";
|
|
9
10
|
apiKeyTransport?: OcxProviderConfig["apiKeyTransport"];
|
|
10
11
|
dashboardUrl: string;
|
|
11
12
|
models?: string[];
|
|
@@ -22,6 +23,7 @@ export interface DerivedKeyLoginProvider {
|
|
|
22
23
|
modelDefaultReasoningEfforts?: Record<string, string>;
|
|
23
24
|
reasoningEffortMap?: Record<string, string>;
|
|
24
25
|
modelReasoningEffortMap?: Record<string, Record<string, string>>;
|
|
26
|
+
reasoningWireFormat?: OcxProviderConfig["reasoningWireFormat"];
|
|
25
27
|
noVisionModels?: string[];
|
|
26
28
|
noReasoningModels?: string[];
|
|
27
29
|
noTemperatureModels?: string[];
|
|
@@ -102,13 +104,20 @@ function cloneNestedRecord(input: Record<string, Record<string, string>>): Recor
|
|
|
102
104
|
return Object.fromEntries(Object.entries(input).map(([key, value]) => [key, { ...value }]));
|
|
103
105
|
}
|
|
104
106
|
|
|
107
|
+
/**
|
|
108
|
+
* Build the provider config a registry entry contributes when a preset is materialized.
|
|
109
|
+
* The registry auth kind is preserved verbatim (including `"local"`) so fail-closed gates
|
|
110
|
+
* keep distinguishing local runtimes from API-key providers after the seed round-trip.
|
|
111
|
+
*/
|
|
105
112
|
export function providerConfigSeed(entry: ProviderRegistryEntry): OcxProviderConfig {
|
|
106
113
|
return {
|
|
107
114
|
adapter: entry.adapter,
|
|
108
115
|
baseUrl: entry.baseUrl,
|
|
109
116
|
...(entry.apiKeyTransport !== undefined ? { apiKeyTransport: entry.apiKeyTransport } : {}),
|
|
110
117
|
...(entry.responsesPath ? { responsesPath: entry.responsesPath } : {}),
|
|
111
|
-
|
|
118
|
+
// Preserve the registry auth kind verbatim (including "local") so fail-closed gates that
|
|
119
|
+
// distinguish local runtimes from API-key providers keep working after the seed round-trip.
|
|
120
|
+
authMode: entry.authKind,
|
|
112
121
|
...(entry.codexAccountMode ? { codexAccountMode: entry.codexAccountMode } : {}),
|
|
113
122
|
...(entry.keyOptional !== undefined ? { keyOptional: entry.keyOptional } : {}),
|
|
114
123
|
...(entry.freeTier !== undefined ? { freeTier: entry.freeTier } : {}),
|
|
@@ -128,6 +137,7 @@ export function providerConfigSeed(entry: ProviderRegistryEntry): OcxProviderCon
|
|
|
128
137
|
...(entry.modelDefaultReasoningEfforts ? { modelDefaultReasoningEfforts: { ...entry.modelDefaultReasoningEfforts } } : {}),
|
|
129
138
|
...(entry.reasoningEffortMap ? { reasoningEffortMap: { ...entry.reasoningEffortMap } } : {}),
|
|
130
139
|
...(entry.modelReasoningEffortMap ? { modelReasoningEffortMap: cloneNestedRecord(entry.modelReasoningEffortMap) } : {}),
|
|
140
|
+
...(entry.reasoningWireFormat ? { reasoningWireFormat: entry.reasoningWireFormat } : {}),
|
|
131
141
|
...(entry.noVisionModels ? { noVisionModels: [...entry.noVisionModels] } : {}),
|
|
132
142
|
...(entry.noReasoningModels ? { noReasoningModels: [...entry.noReasoningModels] } : {}),
|
|
133
143
|
...(entry.noTemperatureModels ? { noTemperatureModels: [...entry.noTemperatureModels] } : {}),
|
|
@@ -159,6 +169,7 @@ export function deriveKeyLoginMap(): Record<string, DerivedKeyLoginProvider> {
|
|
|
159
169
|
baseUrl: entry.baseUrl,
|
|
160
170
|
...(entry.responsesPath ? { responsesPath: entry.responsesPath } : {}),
|
|
161
171
|
adapter: entry.adapter,
|
|
172
|
+
...(entry.apiKeyValidation !== undefined ? { apiKeyValidation: entry.apiKeyValidation } : {}),
|
|
162
173
|
...(entry.apiKeyTransport !== undefined ? { apiKeyTransport: entry.apiKeyTransport } : {}),
|
|
163
174
|
dashboardUrl: entry.dashboardUrl,
|
|
164
175
|
...(entry.models ? { models: [...entry.models] } : {}),
|
|
@@ -175,6 +186,7 @@ export function deriveKeyLoginMap(): Record<string, DerivedKeyLoginProvider> {
|
|
|
175
186
|
...(entry.modelDefaultReasoningEfforts ? { modelDefaultReasoningEfforts: { ...entry.modelDefaultReasoningEfforts } } : {}),
|
|
176
187
|
...(entry.reasoningEffortMap ? { reasoningEffortMap: { ...entry.reasoningEffortMap } } : {}),
|
|
177
188
|
...(entry.modelReasoningEffortMap ? { modelReasoningEffortMap: cloneNestedRecord(entry.modelReasoningEffortMap) } : {}),
|
|
189
|
+
...(entry.reasoningWireFormat ? { reasoningWireFormat: entry.reasoningWireFormat } : {}),
|
|
178
190
|
...(entry.noVisionModels ? { noVisionModels: [...entry.noVisionModels] } : {}),
|
|
179
191
|
...(entry.noReasoningModels ? { noReasoningModels: [...entry.noReasoningModels] } : {}),
|
|
180
192
|
...(entry.noTemperatureModels ? { noTemperatureModels: [...entry.noTemperatureModels] } : {}),
|
|
@@ -248,6 +260,7 @@ export function enrichProviderFromRegistry(name: string, prov: OcxProviderConfig
|
|
|
248
260
|
if (!prov.modelDefaultReasoningEfforts && seed.modelDefaultReasoningEfforts) prov.modelDefaultReasoningEfforts = { ...seed.modelDefaultReasoningEfforts };
|
|
249
261
|
if (!prov.reasoningEffortMap && seed.reasoningEffortMap) prov.reasoningEffortMap = { ...seed.reasoningEffortMap };
|
|
250
262
|
if (!prov.modelReasoningEffortMap && seed.modelReasoningEffortMap) prov.modelReasoningEffortMap = cloneNestedRecord(seed.modelReasoningEffortMap);
|
|
263
|
+
if (prov.reasoningWireFormat === undefined && seed.reasoningWireFormat !== undefined) prov.reasoningWireFormat = seed.reasoningWireFormat;
|
|
251
264
|
if (!prov.noVisionModels && seed.noVisionModels) prov.noVisionModels = [...seed.noVisionModels];
|
|
252
265
|
if (!prov.noReasoningModels && seed.noReasoningModels) prov.noReasoningModels = [...seed.noReasoningModels];
|
|
253
266
|
if (!prov.noTemperatureModels && seed.noTemperatureModels) prov.noTemperatureModels = [...seed.noTemperatureModels];
|
|
@@ -259,6 +272,25 @@ export function enrichProviderFromRegistry(name: string, prov: OcxProviderConfig
|
|
|
259
272
|
// learned this route still gets backfilled.
|
|
260
273
|
if (prov.responsesPath === undefined && seed.responsesPath !== undefined) prov.responsesPath = seed.responsesPath;
|
|
261
274
|
if (prov.statelessResponses === undefined && seed.statelessResponses !== undefined) prov.statelessResponses = seed.statelessResponses;
|
|
275
|
+
// Registry-only metadata (never seeded into saved config): backfill straight from
|
|
276
|
+
// the entry so an explicit user value stays distinguishable from the default.
|
|
277
|
+
if (prov.supportsServiceTier === undefined && entry.supportsServiceTier !== undefined) prov.supportsServiceTier = entry.supportsServiceTier;
|
|
278
|
+
if (prov.preserveResponsesReasoningContent === undefined && entry.preserveResponsesReasoningContent !== undefined) prov.preserveResponsesReasoningContent = entry.preserveResponsesReasoningContent;
|
|
279
|
+
// Registry-only repair policy (#938): fill only when the runtime provider has
|
|
280
|
+
// no explicit policy, and deep-clone so saved/user values never alias the
|
|
281
|
+
// registry constant.
|
|
282
|
+
if (prov.responsesItemIdRepair === undefined && entry.responsesItemIdRepair) {
|
|
283
|
+
prov.responsesItemIdRepair = {
|
|
284
|
+
...(entry.responsesItemIdRepair.message ? { message: [...entry.responsesItemIdRepair.message] } : {}),
|
|
285
|
+
...(entry.responsesItemIdRepair.reasoning ? { reasoning: [...entry.responsesItemIdRepair.reasoning] } : {}),
|
|
286
|
+
...(entry.responsesItemIdRepair.repairMissingTerminalIds !== undefined
|
|
287
|
+
? { repairMissingTerminalIds: entry.responsesItemIdRepair.repairMissingTerminalIds }
|
|
288
|
+
: {}),
|
|
289
|
+
...(entry.responsesItemIdRepair.repairInvalidIds !== undefined
|
|
290
|
+
? { repairInvalidIds: entry.responsesItemIdRepair.repairInvalidIds }
|
|
291
|
+
: {}),
|
|
292
|
+
};
|
|
293
|
+
}
|
|
262
294
|
if (!prov.autoToolChoiceOnlyModels && seed.autoToolChoiceOnlyModels) prov.autoToolChoiceOnlyModels = [...seed.autoToolChoiceOnlyModels];
|
|
263
295
|
if (!prov.preserveReasoningContentModels && seed.preserveReasoningContentModels) prov.preserveReasoningContentModels = [...seed.preserveReasoningContentModels];
|
|
264
296
|
if (!prov.reasoningSplitModels && seed.reasoningSplitModels) prov.reasoningSplitModels = [...seed.reasoningSplitModels];
|