@bitkyc08/opencodex 2.81.0 → 2.83.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/bin/ocx.mjs +166 -73
- package/gui/dist/assets/App-Ba8uG696.js +54 -0
- package/gui/dist/assets/{Tray-EwAOsP0S.js → Tray-IImbNPRy.js} +1 -1
- package/gui/dist/assets/{index-Brk6F3fn.css → index-BAbcj51J.css} +1 -1
- package/gui/dist/assets/{index-RsoaAxXY.js → index-BHRZdJuv.js} +13 -13
- package/gui/dist/assets/{quota-summary-D2lN8tDm.js → quota-summary-DiJO6sYY.js} +1 -1
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic.ts +1 -1
- package/src/adapters/cursor/catalog.ts +2 -2
- package/src/adapters/cursor/effort-map.ts +1 -1
- package/src/adapters/devin/live-models.ts +2 -2
- package/src/adapters/google-truncation.ts +22 -0
- package/src/adapters/google.ts +7 -3
- package/src/adapters/ollama-native.ts +18 -14
- package/src/adapters/openai-responses/compaction-search-history.ts +109 -0
- package/src/adapters/openai-responses/passthrough.ts +11 -4
- package/src/adapters/openai-responses/plaintext-reasoning-notice.ts +85 -0
- package/src/adapters/openai-responses/reasoning.ts +11 -1
- package/src/adapters/openai-responses/tool-output-recovery.ts +20 -0
- package/src/bridge/errors.ts +7 -1
- package/src/claude/agents-inject.ts +14 -3
- package/src/claude/context-windows.ts +22 -20
- package/src/claude/desktop-3p.ts +88 -12
- package/src/claude/desktop-picker.ts +1 -1
- package/src/claude/desktop-profile.ts +2 -1
- package/src/claude/gateway-cache.ts +18 -1
- package/src/claude/inbound.ts +77 -3
- package/src/claude/intercept/cli-catalog.ts +3 -0
- package/src/claude/intercept/picker-ca-persistence.ts +7 -6
- package/src/claude/intercept/picker-models.ts +14 -6
- package/src/claude/intercept/settings.ts +3 -0
- package/src/claude/long-context.ts +66 -0
- package/src/claude/model-info.ts +19 -16
- package/src/claude/outbound.ts +94 -9
- package/src/claude/subagent-model.ts +40 -11
- package/src/cli/account-auth.ts +20 -12
- package/src/cli/agent.ts +9 -5
- package/src/cli/aside-profile-recovery.ts +33 -0
- package/src/cli/capabilities-base.ts +5 -5
- package/src/cli/capabilities-integrations.ts +2 -2
- package/src/cli/cli-path-diagnostics.ts +132 -0
- package/src/cli/desktop-runtime-guidance.ts +65 -0
- package/src/cli/dispatch.ts +5 -0
- package/src/cli/doctor.ts +16 -3
- package/src/cli/index.ts +14 -9
- package/src/cli/integration-aside-sync.ts +2 -1
- package/src/cli/integrations.ts +16 -3
- package/src/cli/resolve.ts +26 -3
- package/src/cli/root.ts +4 -2
- package/src/cli/status.ts +64 -12
- package/src/cli/update-restart-child.ts +4 -2
- package/src/cli/update-restart-home.ts +28 -10
- package/src/cli/update-restart-service-record.ts +116 -0
- package/src/cli/update-restart-supervision.ts +96 -0
- package/src/cli/update-restart.ts +59 -13
- package/src/cli/v2.ts +27 -20
- package/src/cli/version-skew-notice.ts +77 -0
- package/src/clients/config-export/model-metadata.ts +16 -0
- package/src/clients/config-export/zcode-store.ts +46 -11
- package/src/clients/config-export/zcode.ts +3 -5
- package/src/clients/config-export.ts +1 -1
- package/src/clients/lazycodex.ts +1 -1
- package/src/clients/omo-role-models.ts +32 -7
- package/src/codex/auth-api/account-selection.ts +107 -0
- package/src/codex/auth-api/pool-mode-gate.ts +4 -5
- package/src/codex/auth-api/routes.ts +10 -49
- package/src/codex/auth-context.ts +28 -4
- package/src/codex/autostart-health.ts +12 -3
- package/src/codex/catalog/build-entries.ts +18 -0
- package/src/codex/catalog/bundled.ts +155 -26
- package/src/codex/catalog/combo-member.ts +3 -2
- package/src/codex/catalog/effort.ts +26 -5
- package/src/codex/catalog/gather-capture.ts +5 -0
- package/src/codex/catalog/model-hints.ts +15 -8
- package/src/codex/catalog/model-visibility.ts +3 -1
- package/src/codex/catalog/native-models.ts +1 -1
- package/src/codex/catalog/operator-tiers.ts +38 -0
- package/src/codex/catalog/provider-models.ts +68 -35
- package/src/codex/catalog/retained-sync.ts +7 -5
- package/src/codex/catalog/routed-gather.ts +15 -1
- package/src/codex/catalog-auto-refresh-sources.ts +3 -4
- package/src/codex/catalog-auto-refresh.ts +19 -0
- package/src/codex/catalog.ts +1 -1
- package/src/codex/codex-home-owner.ts +3 -0
- package/src/codex/config-write-lock.ts +333 -0
- package/src/codex/convergence.ts +2 -1
- package/src/codex/desktop-app/darwin.ts +1 -1
- package/src/codex/desktop-app/handoff.ts +1 -1
- package/src/codex/desktop-app/linux.ts +1 -1
- package/src/codex/desktop-app/lock.ts +1 -1
- package/src/codex/desktop-app/types.ts +1 -1
- package/src/codex/desktop-app/windows.ts +1 -1
- package/src/codex/desktop-app-restart.ts +1 -1
- package/src/codex/features.ts +257 -200
- package/src/codex/inject/config-write-section.ts +34 -0
- package/src/codex/inject/multi-agent-v2.ts +14 -6
- package/src/codex/inject/remove.ts +36 -11
- package/src/codex/inject/restore.ts +85 -20
- package/src/codex/inject-coordination.ts +15 -6
- package/src/codex/inject.ts +62 -20
- package/src/codex/journal.ts +62 -17
- package/src/codex/main-account-cache.ts +43 -0
- package/src/codex/main-account.ts +6 -7
- package/src/codex/paths.ts +2 -0
- package/src/codex/prepared-home.ts +18 -0
- package/src/codex/prompt-journal.ts +25 -12
- package/src/codex/prompt-layers.ts +49 -26
- package/src/codex/prompt-lock-claim.ts +134 -0
- package/src/codex/prompt-lock-evidence.ts +26 -0
- package/src/codex/prompt-lock-io.ts +16 -0
- package/src/codex/prompt-lock-owner.ts +130 -0
- package/src/codex/prompt-lock.ts +84 -53
- package/src/codex/routing/failure-window.ts +146 -0
- package/src/codex/routing/selection.ts +4 -1
- package/src/codex/routing.ts +13 -17
- package/src/codex/runtime.ts +106 -18
- package/src/codex/shim.ts +45 -19
- package/src/combos/jev-service-exchange.ts +281 -0
- package/src/combos/jev.ts +34 -220
- package/src/config/atomic-write.ts +1 -1
- package/src/config/codex-ws-reuse-setting.ts +10 -0
- package/src/config/live-reconcile.ts +17 -0
- package/src/config/load-degrade.ts +8 -0
- package/src/config/rebase-provenance.ts +11 -0
- package/src/config/schema/config-schema.ts +4 -0
- package/src/generated/compatibility-version.json +375 -227
- package/src/generated/model-metadata.ts +2 -2
- package/src/grok/inject.ts +276 -206
- package/src/grok/reset-coupon-ledger.ts +223 -57
- package/src/grok/reset-coupons.ts +13 -6
- package/src/grok/sync.ts +2 -1
- package/src/images/loop.ts +29 -11
- package/src/integrations/aside-profiles.ts +10 -3
- package/src/integrations/catalog-refresh.ts +18 -2
- package/src/integrations/cursor-detect.ts +1 -1
- package/src/integrations/writer.ts +9 -3
- package/src/lib/bun-path-runtime.d.mts +32 -0
- package/src/lib/bun-path-runtime.mjs +95 -0
- package/src/lib/bun-runtime-preflight.ts +140 -0
- package/src/lib/desktop-cli-handoff.d.mts +20 -0
- package/src/lib/desktop-cli-handoff.mjs +92 -0
- package/src/lib/desktop-cli-record.d.mts +29 -0
- package/src/lib/desktop-cli-record.mjs +102 -0
- package/src/lib/errors.ts +25 -0
- package/src/lib/redact.ts +2 -2
- package/src/lib/request-execution-budget.ts +8 -0
- package/src/lib/synced-state-location.ts +46 -6
- package/src/lib/test-home-guard.ts +87 -1
- package/src/lib/translator-budget.ts +17 -4
- package/src/lib/windows-atomic-replace.ts +38 -16
- package/src/lib/windows-elevation.ts +6 -0
- package/src/lib/windows-secret-acl.ts +92 -25
- package/src/lib/winsw.ts +2 -3
- package/src/oauth/anthropic-account-refusal.ts +39 -1
- package/src/oauth/anthropic-routing.ts +1 -1
- package/src/oauth/anthropic-send-ownership.ts +3 -3
- package/src/oauth/chatgpt.ts +113 -41
- package/src/oauth/index.ts +7 -3
- package/src/oauth/meta-muse-device.ts +1 -1
- package/src/oauth/store.ts +1 -1
- package/src/oauth/types.ts +1 -1
- package/src/providers/anthropic-fast.ts +1 -1
- package/src/providers/anthropic-reset-grant-ledger.ts +1 -1
- package/src/providers/azure-model-metadata.ts +115 -0
- package/src/providers/derive.ts +21 -0
- package/src/providers/devin-cli-authmode-migration.ts +1 -1
- package/src/providers/devin-provider-merge-migration.ts +1 -1
- package/src/providers/github-copilot-auto.ts +169 -0
- package/src/providers/model-presets.ts +1 -1
- package/src/providers/muse-subscription-usage.ts +1 -1
- package/src/providers/quota/vendor-probes-key.ts +127 -17
- package/src/providers/quota.ts +1 -1
- package/src/providers/registry/entries-core.ts +6 -6
- package/src/providers/registry/entries-extended.ts +4 -0
- package/src/providers/registry/model-seeds.ts +3 -3
- package/src/providers/xai-fast-model.ts +1 -1
- package/src/responses/reasoning-envelope.ts +29 -1
- package/src/responses/reasoning-replay-cache.ts +17 -1
- package/src/responses/spill-store.ts +15 -2
- package/src/responses/state/metrics.ts +8 -0
- package/src/responses/state/spill-failure.ts +12 -0
- package/src/responses/state/spill-queue.ts +12 -5
- package/src/responses/state.ts +18 -10
- package/src/responses/task-input.ts +17 -0
- package/src/server/background-lifecycle.ts +27 -0
- package/src/server/chat-completions.ts +1 -0
- package/src/server/chat-native.ts +28 -12
- package/src/server/claude-messages.ts +43 -6
- package/src/server/inbound-body-admission.ts +25 -7
- package/src/server/index/claude-intercept-lifecycle.ts +3 -1
- package/src/server/index/link-listener.ts +30 -17
- package/src/server/index/serve-options.ts +18 -4
- package/src/server/index/spend-ledger-lifecycle.ts +2 -1
- package/src/server/index/startup-warnings.ts +19 -0
- package/src/server/index.ts +2 -1
- package/src/server/lifecycle.ts +31 -4
- package/src/server/management/agent-settings-routes.ts +93 -32
- package/src/server/management/codex-agent-role-routes.ts +4 -2
- package/src/server/management/codex-prompt-routes.ts +1 -0
- package/src/server/management/codex-role-auto-assign.ts +13 -2
- package/src/server/management/config-routes.ts +59 -8
- package/src/server/management/context.ts +2 -2
- package/src/server/management/grok-coupon-routes.ts +277 -52
- package/src/server/management/link-routes.ts +42 -20
- package/src/server/management/route-registry.ts +2 -2
- package/src/server/management/shared.ts +3 -3
- package/src/server/messages-native-oauth.ts +9 -8
- package/src/server/messages-native.ts +49 -13
- package/src/server/messages-response-headers.ts +14 -0
- package/src/server/relay-eager.ts +2 -0
- package/src/server/relay.ts +18 -7
- package/src/server/request-log-conversation.ts +48 -5
- package/src/server/request-log-failure-attribution.ts +3 -0
- package/src/server/request-log-terminal-status.ts +186 -0
- package/src/server/request-log.ts +15 -89
- package/src/server/responses/adapter-continuation.ts +11 -3
- package/src/server/responses/adapter-dispatch.ts +99 -19
- package/src/server/responses/codex-ws-exchange.ts +17 -2
- package/src/server/responses/codex-ws-pool.ts +100 -13
- package/src/server/responses/codex-ws-wire.ts +9 -0
- package/src/server/responses/combo-requested-effort.ts +3 -1
- package/src/server/responses/compact.ts +3 -2
- package/src/server/responses/core-codex-account.ts +22 -1
- package/src/server/responses/core-combo-native.ts +20 -3
- package/src/server/responses/core-combo.ts +22 -14
- package/src/server/responses/core-lifetime.ts +3 -36
- package/src/server/responses/core-opaque-recovery.ts +5 -5
- package/src/server/responses/core-options.ts +5 -0
- package/src/server/responses/core-replay.ts +73 -0
- package/src/server/responses/core.ts +9 -9
- package/src/server/responses/empty-completion-guard.ts +18 -2
- package/src/server/responses/fetch-helpers.ts +26 -3
- package/src/server/responses/jev-model-invoke.ts +1 -1
- package/src/server/responses/passthrough-delivery.ts +24 -2
- package/src/server/responses/passthrough-dispatch.ts +134 -62
- package/src/server/responses/passthrough-execution.ts +3 -3
- package/src/server/responses/request-prepare.ts +4 -0
- package/src/server/responses/request-send-budget.ts +112 -49
- package/src/server/responses/request-transport.ts +125 -5
- package/src/server/responses/response-effects.ts +1 -0
- package/src/server/responses/run-turn-execution.ts +15 -5
- package/src/server/responses/sidecar-execution.ts +30 -16
- package/src/server/responses/sidecar-send-budget.ts +58 -0
- package/src/server/responses/spend-storage-error.ts +33 -0
- package/src/server/responses/ws-upstream.ts +19 -6
- package/src/server/startup-health-cache.ts +2 -1
- package/src/server/workflow-refusal.ts +3 -0
- package/src/service/cli.ts +20 -4
- package/src/service/desktop-command-guard.ts +32 -0
- package/src/service/desktop-startup.ts +66 -40
- package/src/service/desktop-supervision.d.mts +26 -0
- package/src/service/desktop-supervision.mjs +140 -0
- package/src/service/managing-cli.ts +1 -0
- package/src/service/orchestration.ts +46 -20
- package/src/service/repair.ts +25 -9
- package/src/service/state.ts +4 -3
- package/src/service/systemd.ts +17 -8
- package/src/service/windows-ops.ts +61 -22
- package/src/storage/policy-job.ts +5 -1
- package/src/storage/policy-worker.ts +3 -1
- package/src/storage/policy.ts +9 -0
- package/src/types/config.ts +21 -0
- package/src/types/request.ts +10 -0
- package/src/types/wire.ts +1 -0
- package/src/update/index.ts +57 -9
- package/src/update/restart-ownership.ts +10 -3
- package/src/update/runtime-ownership.d.mts +6 -0
- package/src/update/runtime-ownership.mjs +23 -7
- package/src/usage/expected-prices.ts +1 -1
- package/src/web-search/loop.ts +5 -2
- package/gui/dist/assets/App-7yhoys8a.js +0 -54
|
@@ -3,7 +3,7 @@ import { anthropicModelFamily } from "./anthropic-model-quota";
|
|
|
3
3
|
import { readBoundedResponseBody } from "../lib/bounded-body";
|
|
4
4
|
import { classifyAnthropic429, anthropicRetryAfterMs, anthropicRatePolicyFor, ANTHROPIC_SHORT_RETRY_MS, ANTHROPIC_MAX_INLINE_THROTTLE_MS } from "./anthropic-rate-limit-policy";
|
|
5
5
|
import { isNonReplayableResponse, sleepWithAbort } from "../lib/upstream-retry";
|
|
6
|
-
import { credentialGeneration, getAccountCredentialWithStatus } from "./store";
|
|
6
|
+
import { credentialGeneration, getAccountCredentialWithStatus, markAccountNeedsReauthIfGeneration } from "./store";
|
|
7
7
|
import type { OAuthAccessSnapshot } from "./index";
|
|
8
8
|
import type { OcxConfig } from "../types";
|
|
9
9
|
import type { AnthropicRouteDecision } from "./anthropic-model-routes";
|
|
@@ -31,6 +31,22 @@ export function bindAnthropicRefusalCredentialForSend(response: Response, owner:
|
|
|
31
31
|
responseCredentials.set(response, Object.freeze({ ...owner, providerAccountUuid, checkProviderUuid: arguments.length >= 3 }));
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
async function isRevokedOAuthToken(response: Response, signal?: AbortSignal): Promise<boolean> {
|
|
35
|
+
if (response.status !== 401) return false;
|
|
36
|
+
try {
|
|
37
|
+
const body = await readBoundedResponseBody(response.clone(), { signal, fatalUtf8: true });
|
|
38
|
+
if (!body.displaySafe || body.truncated) return false;
|
|
39
|
+
const payload: unknown = JSON.parse(body.text);
|
|
40
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)
|
|
41
|
+
|| !("type" in payload) || payload.type !== "error" || !("error" in payload)) return false;
|
|
42
|
+
const error = payload.error;
|
|
43
|
+
return !!error && typeof error === "object" && !Array.isArray(error)
|
|
44
|
+
&& "type" in error && error.type === "authentication_error"
|
|
45
|
+
&& "message" in error && error.message === "OAuth access token has been revoked."
|
|
46
|
+
&& (!("code" in error) || error.code == null);
|
|
47
|
+
} catch { return false; }
|
|
48
|
+
}
|
|
49
|
+
|
|
34
50
|
async function isAccountRefusal(response: Response, signal?: AbortSignal): Promise<boolean> {
|
|
35
51
|
try {
|
|
36
52
|
const body = await readBoundedResponseBody(response.clone(), { signal });
|
|
@@ -91,6 +107,28 @@ export async function rotateAnthropicAccountOnResponseForInstance(
|
|
|
91
107
|
&& credentialGeneration(row.credential) === sent.generation
|
|
92
108
|
&& (!sent.checkProviderUuid || row.credential.accountId === sent.providerAccountUuid) ? row : undefined;
|
|
93
109
|
};
|
|
110
|
+
if (response.status === 401) {
|
|
111
|
+
if (options.allowAccountRefusal === false) return null;
|
|
112
|
+
let verdict = verdicts.get(response);
|
|
113
|
+
if (!verdict) { verdict = isRevokedOAuthToken(response, options.signal); verdicts.set(response, verdict); }
|
|
114
|
+
if (!await verdict || options.signal?.aborted || !ownedCurrent()) return null;
|
|
115
|
+
options.currentDecision?.();
|
|
116
|
+
let marked: boolean;
|
|
117
|
+
try {
|
|
118
|
+
marked = await markAccountNeedsReauthIfGeneration(instance, sent.accountId, sent.generation, undefined, undefined, store => {
|
|
119
|
+
const row = store[instance]?.accounts.find(account => account.id === sent.accountId);
|
|
120
|
+
return !options.signal?.aborted && configuredAnthropicInstance(options.config, instance) === instance
|
|
121
|
+
&& anthropicPhysicalSendOwnershipIsCurrent(sent, store)
|
|
122
|
+
&& (!sent.checkProviderUuid || row?.credential.accountId === sent.providerAccountUuid);
|
|
123
|
+
});
|
|
124
|
+
} catch { return null; }
|
|
125
|
+
if (!marked || configuredAnthropicInstance(options.config, instance) !== instance
|
|
126
|
+
|| !anthropicPhysicalSendOwnershipIsCurrent(sent)) return null;
|
|
127
|
+
routing.clearAnthropicSessionAffinityForAccount(sent.accountId);
|
|
128
|
+
if (!options.canRetry || options.signal?.aborted) return null;
|
|
129
|
+
const decision = options.currentDecision ? options.currentDecision() : options.decision ?? null;
|
|
130
|
+
return pickAlternateAnthropicAccount(options.config, sent.accountId, Date.now(), decision, options.model, options.excludedAccountIds);
|
|
131
|
+
}
|
|
94
132
|
if (response.status === 429) {
|
|
95
133
|
const current = ownedCurrent();
|
|
96
134
|
if (!current) return null;
|
|
@@ -704,7 +704,7 @@ function createAnthropicRouting(instance: AnthropicInstanceId): AnthropicRouting
|
|
|
704
704
|
excludedAccountIds?: ReadonlySet<string>,
|
|
705
705
|
): string | null {
|
|
706
706
|
if (!admitted(config)) return null;
|
|
707
|
-
const strategy = anthropicPoolStrategy(config);
|
|
707
|
+
const strategy = isAnthropicAccountPoolEnabled(config) ? anthropicPoolStrategy(config) : "quota";
|
|
708
708
|
const eligible = routeCandidates(getEligibleAnthropicAccounts(now, model), decision).filter(id => id !== excludeId && !excludedAccountIds?.has(id));
|
|
709
709
|
if (strategy === "round-robin") {
|
|
710
710
|
return peekRoundRobinAccount(poolKey, eligible, stickyLimitForPool(config));
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** Ownership captured before a physical send, independent of subsequent cooldown observations. */
|
|
2
2
|
import type { OAuthAccessSnapshot } from "./index";
|
|
3
|
-
import { credentialGeneration, getAccountSet } from "./store";
|
|
3
|
+
import { credentialGeneration, getAccountSet, type AuthStore } from "./store";
|
|
4
4
|
import { isAnthropicInstanceId, type AnthropicInstanceId } from "../providers/anthropic-instance-id";
|
|
5
5
|
import { anthropicCooldownRecoveryFor } from "../providers/quota/anthropic-cooldown-recovery";
|
|
6
6
|
import { captureProviderAccountQuotaEpoch } from "../providers/quota/account-cache";
|
|
@@ -32,8 +32,8 @@ export function captureAnthropicPhysicalSendOwnership(snapshot: OAuthAccessSnaps
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
/** Pure ownership read: never adopt or reserve the replacement account's current incarnation. */
|
|
35
|
-
export function anthropicPhysicalSendOwnershipIsCurrent(owner: AnthropicPhysicalSendOwnership): boolean {
|
|
36
|
-
const row = getAccountSet(owner.provider)?.accounts.find(account => account.id === owner.accountId);
|
|
35
|
+
export function anthropicPhysicalSendOwnershipIsCurrent(owner: AnthropicPhysicalSendOwnership, store?: AuthStore): boolean {
|
|
36
|
+
const row = (store ? store[owner.provider] : getAccountSet(owner.provider))?.accounts.find(account => account.id === owner.accountId);
|
|
37
37
|
return !!row && row.loginId === owner.loginId && row.addedAt === owner.addedAt
|
|
38
38
|
&& row.credential.access === owner.accessToken && credentialGeneration(row.credential) === owner.generation
|
|
39
39
|
&& anthropicCooldownRecoveryFor(owner.provider).anthropicAccountIncarnation(owner.accountId) === owner.accountIncarnation
|
package/src/oauth/chatgpt.ts
CHANGED
|
@@ -1,6 +1,106 @@
|
|
|
1
1
|
import { OAuthCallbackFlow } from "./callback-server";
|
|
2
2
|
import type { OAuthController, OAuthCredentials } from "./types";
|
|
3
3
|
import { generatePKCE } from "./pkce";
|
|
4
|
+
import { classifyChatgptRefreshFailure } from "../codex/chatgpt-refresh-failure";
|
|
5
|
+
import { readBoundedResponseBytes } from "../lib/bounded-body";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Per-fetch deadline for ChatGPT auth endpoint calls. The deviceauth grant got the
|
|
9
|
+
* same bound in #3898 after one stuck TCP connection held the login slot for the
|
|
10
|
+
* whole grant; a hung refresh would pin the refresh intent lock indefinitely.
|
|
11
|
+
*/
|
|
12
|
+
export const CHATGPT_FETCH_TIMEOUT_MS = 30_000;
|
|
13
|
+
|
|
14
|
+
/** Non-HTTP failures retain no caller reason, upstream error, or cause. */
|
|
15
|
+
export class ChatGptTokenRequestError extends Error {
|
|
16
|
+
readonly terminal = false;
|
|
17
|
+
|
|
18
|
+
constructor(kind: "abort" | "timeout" | "transport") {
|
|
19
|
+
super(kind === "abort" ? "ChatGPT token request cancelled"
|
|
20
|
+
: kind === "timeout" ? "ChatGPT token request timed out" : "ChatGPT token request failed");
|
|
21
|
+
this.name = kind === "abort" ? "AbortError" : kind === "timeout" ? "TimeoutError" : "ChatGptTokenRequestError";
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const CHATGPT_ERROR_BODY_MAX_BYTES = 16 * 1024;
|
|
26
|
+
const CHATGPT_SUCCESS_BODY_MAX_BYTES = 64 * 1024;
|
|
27
|
+
|
|
28
|
+
/** One deadline covers headers and body; every settled call detaches its caller. */
|
|
29
|
+
async function chatGptTokenRequest(
|
|
30
|
+
body: URLSearchParams,
|
|
31
|
+
label: "ChatGPT refresh" | "ChatGPT token exchange",
|
|
32
|
+
options: { signal?: AbortSignal; timeoutMs?: number },
|
|
33
|
+
): Promise<OAuthCredentials> {
|
|
34
|
+
const controller = new AbortController();
|
|
35
|
+
const onAbort = () => controller.abort(new ChatGptTokenRequestError("abort"));
|
|
36
|
+
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
37
|
+
if (options.signal?.aborted) onAbort();
|
|
38
|
+
const timer = setTimeout(() => controller.abort(new ChatGptTokenRequestError("timeout")),
|
|
39
|
+
options.timeoutMs ?? CHATGPT_FETCH_TIMEOUT_MS);
|
|
40
|
+
let endpointError: ChatGptTokenError | undefined;
|
|
41
|
+
try {
|
|
42
|
+
controller.signal.throwIfAborted();
|
|
43
|
+
const response = await fetch(TOKEN_URL, {
|
|
44
|
+
method: "POST",
|
|
45
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
46
|
+
body: body.toString(),
|
|
47
|
+
signal: controller.signal,
|
|
48
|
+
});
|
|
49
|
+
const result = await readBoundedResponseBytes(response, {
|
|
50
|
+
signal: controller.signal,
|
|
51
|
+
maxBytes: response.ok ? CHATGPT_SUCCESS_BODY_MAX_BYTES : CHATGPT_ERROR_BODY_MAX_BYTES,
|
|
52
|
+
});
|
|
53
|
+
controller.signal.throwIfAborted();
|
|
54
|
+
const text = new TextDecoder().decode(result.bytes);
|
|
55
|
+
if (!response.ok) {
|
|
56
|
+
endpointError = chatGptTokenError(response.status, result.oversized ? "" : text, label);
|
|
57
|
+
throw endpointError;
|
|
58
|
+
}
|
|
59
|
+
if (result.oversized) throw new ChatGptTokenRequestError("transport");
|
|
60
|
+
return credsFromToken(JSON.parse(text) as Record<string, unknown>);
|
|
61
|
+
} catch (error) {
|
|
62
|
+
if (controller.signal.aborted) throw controller.signal.reason;
|
|
63
|
+
if (endpointError && error === endpointError) throw endpointError;
|
|
64
|
+
throw new ChatGptTokenRequestError("transport");
|
|
65
|
+
} finally {
|
|
66
|
+
clearTimeout(timer);
|
|
67
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Token-endpoint failure carrying the HTTP status, the allowlisted OAuth error code,
|
|
73
|
+
* and a terminal verdict requiring an explicit grant-refusal code. The shared
|
|
74
|
+
* Codex classifier supplies the code vocabulary; free text is never grant evidence.
|
|
75
|
+
*/
|
|
76
|
+
export class ChatGptTokenError extends Error {
|
|
77
|
+
constructor(
|
|
78
|
+
public readonly httpStatus: number,
|
|
79
|
+
public readonly oauthError: string | undefined,
|
|
80
|
+
public readonly terminal: boolean,
|
|
81
|
+
message: string,
|
|
82
|
+
) {
|
|
83
|
+
super(message);
|
|
84
|
+
this.name = "ChatGptTokenError";
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function chatGptTokenError(status: number, body: string, label: string): ChatGptTokenError {
|
|
89
|
+
const failure = classifyChatgptRefreshFailure(status, body);
|
|
90
|
+
// Reclassify the allowlisted code alone to reuse the shared terminal vocabulary
|
|
91
|
+
// without its description-only compatibility rule for other Codex callers.
|
|
92
|
+
const terminal = failure.reason !== "unknown" && failure.code !== undefined
|
|
93
|
+
&& classifyChatgptRefreshFailure(status, JSON.stringify({ error: failure.code })).reason !== "unknown";
|
|
94
|
+
// The message carries the status and the allowlisted OAuth code only — never the
|
|
95
|
+
// free-text `error_description`, which can echo OAuth material into log surfaces
|
|
96
|
+
// (the same closed vocabulary the pool's noteChatgptRefreshFailure logs with).
|
|
97
|
+
return new ChatGptTokenError(
|
|
98
|
+
status,
|
|
99
|
+
failure.code,
|
|
100
|
+
terminal,
|
|
101
|
+
`${label} failed: ${status} code=${failure.code ?? "none"}`,
|
|
102
|
+
);
|
|
103
|
+
}
|
|
4
104
|
|
|
5
105
|
const CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
|
|
6
106
|
const AUTH_URL = "https://auth.openai.com/oauth/authorize";
|
|
@@ -202,34 +302,16 @@ export class ChatGPTOAuthFlow extends OAuthCallbackFlow {
|
|
|
202
302
|
|
|
203
303
|
async exchangeToken(code: string, _state: string, redirectUri: string): Promise<OAuthCredentials> {
|
|
204
304
|
if (!this.#verifier) throw new Error("ChatGPT PKCE verifier not initialized");
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
redirect_uri: redirectUri,
|
|
213
|
-
code_verifier: this.#verifier,
|
|
214
|
-
}).toString(),
|
|
215
|
-
});
|
|
216
|
-
if (!resp.ok) {
|
|
217
|
-
const errDesc = await safeErrorDescription(resp);
|
|
218
|
-
throw new Error(`ChatGPT token exchange failed: ${resp.status} ${errDesc}`);
|
|
219
|
-
}
|
|
220
|
-
return credsFromToken((await resp.json()) as Record<string, unknown>);
|
|
305
|
+
return chatGptTokenRequest(new URLSearchParams({
|
|
306
|
+
grant_type: "authorization_code",
|
|
307
|
+
client_id: CLIENT_ID,
|
|
308
|
+
code,
|
|
309
|
+
redirect_uri: redirectUri,
|
|
310
|
+
code_verifier: this.#verifier,
|
|
311
|
+
}), "ChatGPT token exchange", { signal: this.ctrl.signal });
|
|
221
312
|
}
|
|
222
313
|
}
|
|
223
314
|
|
|
224
|
-
function safeErrorDescription(resp: Response): Promise<string> {
|
|
225
|
-
return resp.text().catch(() => "").then(text => {
|
|
226
|
-
try {
|
|
227
|
-
const parsed = JSON.parse(text) as { error?: string; error_description?: string };
|
|
228
|
-
return [parsed.error, parsed.error_description].filter(Boolean).join(": ") || `HTTP ${resp.status}`;
|
|
229
|
-
} catch { return `HTTP ${resp.status}`; }
|
|
230
|
-
});
|
|
231
|
-
}
|
|
232
|
-
|
|
233
315
|
/**
|
|
234
316
|
* How the user proves identity. `browser` runs the localhost:1455 callback flow;
|
|
235
317
|
* `device` runs the deviceauth grant, which needs no local browser or listener
|
|
@@ -255,21 +337,11 @@ export async function loginChatGPT(
|
|
|
255
337
|
// Codex-rs uses JSON for refresh — intentional divergence; both accepted by auth.openai.com.
|
|
256
338
|
export async function refreshChatGPTToken(
|
|
257
339
|
refreshToken: string,
|
|
258
|
-
options: { signal?: AbortSignal } = {},
|
|
340
|
+
options: { signal?: AbortSignal; timeoutMs?: number } = {},
|
|
259
341
|
): Promise<OAuthCredentials> {
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
client_id: CLIENT_ID,
|
|
266
|
-
refresh_token: refreshToken,
|
|
267
|
-
}).toString(),
|
|
268
|
-
signal: options.signal,
|
|
269
|
-
});
|
|
270
|
-
if (!resp.ok) {
|
|
271
|
-
const errDesc = await safeErrorDescription(resp);
|
|
272
|
-
throw new Error(`ChatGPT refresh failed: ${resp.status} ${errDesc}`);
|
|
273
|
-
}
|
|
274
|
-
return credsFromToken((await resp.json()) as Record<string, unknown>);
|
|
342
|
+
return chatGptTokenRequest(new URLSearchParams({
|
|
343
|
+
grant_type: "refresh_token",
|
|
344
|
+
client_id: CLIENT_ID,
|
|
345
|
+
refresh_token: refreshToken,
|
|
346
|
+
}), "ChatGPT refresh", options);
|
|
275
347
|
}
|
package/src/oauth/index.ts
CHANGED
|
@@ -38,7 +38,7 @@ import { anthropicOAuthDefinition, assertAnthropicInstanceLoginConfig, Anthropic
|
|
|
38
38
|
resumeAnthropicRefreshIntentCleanup, clearObservedAnthropicRefreshIntent } from "./anthropic-oauth-definitions";
|
|
39
39
|
import { loginKimi, refreshKimiToken } from "./kimi";
|
|
40
40
|
import { loginNous, NousTokenError, refreshNousToken, clearNousRefreshIntent, RefreshIntentIOError } from "./nous";
|
|
41
|
-
import { loginChatGPT, refreshChatGPTToken, type ChatGPTLoginFlow } from "./chatgpt";
|
|
41
|
+
import { ChatGptTokenRequestError, ChatGptTokenError, loginChatGPT, refreshChatGPTToken, type ChatGPTLoginFlow } from "./chatgpt";
|
|
42
42
|
import { loginAntigravity, refreshAntigravityToken } from "./google-antigravity";
|
|
43
43
|
import { loginCursor, refreshCursorToken } from "./cursor";
|
|
44
44
|
import { loginZed, refreshZedToken } from "./zed";
|
|
@@ -350,7 +350,7 @@ export const OAUTH_PROVIDERS: Record<string, OAuthProviderDef> = {
|
|
|
350
350
|
},
|
|
351
351
|
chatgpt: {
|
|
352
352
|
login: (ctrl, opts) => loginChatGPT(ctrl, { forceLogin: opts?.forceLogin, flow: opts?.flow }),
|
|
353
|
-
refresh: (rt) => refreshChatGPTToken(rt),
|
|
353
|
+
refresh: (rt, signal) => refreshChatGPTToken(rt, { signal }),
|
|
354
354
|
providerConfig: { adapter: "openai-responses", baseUrl: "https://chatgpt.com/backend-api/codex", authMode: "forward" as const },
|
|
355
355
|
defaultModel: "gpt-5.6-luna",
|
|
356
356
|
},
|
|
@@ -360,7 +360,7 @@ export const OAUTH_PROVIDERS: Record<string, OAuthProviderDef> = {
|
|
|
360
360
|
* Removed provider ids that still name a live successor.
|
|
361
361
|
*
|
|
362
362
|
* `devin-cli` was merged into `devin` (import-first login absorbed the CLI
|
|
363
|
-
* credential import; devlog/
|
|
363
|
+
* credential import; devlog/_fin/260913_devin_provider_merge). The id can
|
|
364
364
|
* still arrive here from a saved config row or a stored credential slot that
|
|
365
365
|
* the startup migration has not rekeyed yet, and from a user typing the old
|
|
366
366
|
* name at `ocx login`. It is deliberately NOT an OAUTH_PROVIDERS entry:
|
|
@@ -712,10 +712,14 @@ function isTerminalRefreshError(err: unknown): boolean {
|
|
|
712
712
|
|| msg.includes("expired_token");
|
|
713
713
|
}
|
|
714
714
|
function terminal(error:unknown):boolean{
|
|
715
|
+
if (error instanceof ChatGptTokenRequestError) return false;
|
|
715
716
|
if(error instanceof XaiTokenRequestError)return ["invalid_grant","refresh_token_reused","revoked_token"].includes(error.oauthError??"");
|
|
716
717
|
if(error instanceof AnthropicTokenError)return (error.httpStatus===400||error.httpStatus===401)&&["invalid_grant","refresh_token_reused","revoked","revoked_token","refresh_token_revoked"].includes(error.oauthError??"");
|
|
717
718
|
if(error instanceof KiroTokenRefreshError)return (error.httpStatus===400||error.httpStatus===401)&&error.oauthError!==undefined;
|
|
718
719
|
if(error instanceof NousTokenError)return error.terminal===true||["invalid_grant","refresh_token_reused","revoked","revoked_token","expired_token"].includes(error.oauthError??"");
|
|
720
|
+
// ChatGPT carries the pool classifier's verdict on the error itself: revoked and
|
|
721
|
+
// expired grants are terminal; unknown/transient failures stay retryable.
|
|
722
|
+
if(error instanceof ChatGptTokenError)return error.terminal;
|
|
719
723
|
// Local durable-write/read/cleanup failures are operational, not credential
|
|
720
724
|
// death: the provider credential was never rejected or consumed. Never mark
|
|
721
725
|
// the account needsReauth for broken local persistence infrastructure.
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* resulting account token into the `LLM|` Model API key our request path sends as a
|
|
7
7
|
* bearer.
|
|
8
8
|
*
|
|
9
|
-
* Protocol source: devlog/
|
|
9
|
+
* Protocol source: devlog/_fin/260912_muse_device_oauth/001_reference_measurements.md.
|
|
10
10
|
* It is second-party, not vendor documentation, so every response is parsed defensively
|
|
11
11
|
* and every failure names a kind instead of throwing a bare string.
|
|
12
12
|
*
|
package/src/oauth/store.ts
CHANGED
|
@@ -1508,7 +1508,7 @@ export async function markAccountNeedsReauth(
|
|
|
1508
1508
|
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");if(isAnthropicInstanceId(provider))assertAnthropicCredentialSource(provider,safe);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.refreshAttentionGeneration;if(account.needsReauthReason!=="verify_account"){delete account.needsReauth;delete account.needsReauthReason;}return{superseded:false};},[provider,accountId,safe,opts.expectedGeneration]);}
|
|
1509
1509
|
// A late refresh failure must not change an operator-paused account's health. Check under
|
|
1510
1510
|
// the mutation lock, not before awaiting it, so a concurrent pause cannot be overwritten.
|
|
1511
|
-
export async function markAccountNeedsReauthIfGeneration(provider:string,accountId:string,generation:string,writerGeneration=captureConfigGeneration(),reason?:"verify_account"):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||account.paused||credentialGeneration(account.credential)!==generation)return false;if(writerGeneration<lastReconciledGeneration&&!liveOAuthAccountKeys.has(key))return false;account.needsReauth=true;if(reason==="verify_account")account.needsReauthReason=reason;return true;},[provider,accountId,generation]);}
|
|
1511
|
+
export async function markAccountNeedsReauthIfGeneration(provider:string,accountId:string,generation:string,writerGeneration=captureConfigGeneration(),reason?:"verify_account",isCurrent?:(store:AuthStore)=>boolean):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||account.paused||credentialGeneration(account.credential)!==generation)return false;if(writerGeneration<lastReconciledGeneration&&!liveOAuthAccountKeys.has(key))return false;if(isCurrent&&!isCurrent(store))return false;account.needsReauth=true;if(reason==="verify_account")account.needsReauthReason=reason;return true;},[provider,accountId,generation]);}
|
|
1512
1512
|
|
|
1513
1513
|
/** Attention is projected only; refresh selection continues to use the stored terminal flag. */
|
|
1514
1514
|
export function accountNeedsReauthForStatus(provider: string, account: ProviderAccount, now = Date.now()): boolean {
|
package/src/oauth/types.ts
CHANGED
|
@@ -30,7 +30,7 @@ export interface KiroOAuthMetadata {
|
|
|
30
30
|
*
|
|
31
31
|
* The Model API is authenticated by the `LLM|` key in `access`; this token authenticates
|
|
32
32
|
* the Meta ACCOUNT and exists only to mint that key and to read subscription usage
|
|
33
|
-
* (devlog/
|
|
33
|
+
* (devlog/_fin/260912_muse_device_oauth/002 A). Keeping it out of `access` is what lets
|
|
34
34
|
* every request path stay unchanged.
|
|
35
35
|
*
|
|
36
36
|
* It must never be added to `OAuthAccountSummary` (src/oauth/index.ts:1803) or to
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* account rate limit and must keep flowing into the existing wait/rotation handling, so only an
|
|
13
13
|
* error message that names fast mode or the speed parameter, or a fast-pool header reporting
|
|
14
14
|
* zero remaining, counts. Live probe strings (2026-09-23,
|
|
15
|
-
* devlog/
|
|
15
|
+
* devlog/_fin/260923_anthropic_fast_speed/020_probe-evidence.md):
|
|
16
16
|
* 429 "Usage credits are required for fast mode."
|
|
17
17
|
* 400 "Fast mode is not enabled for your organization. ..."
|
|
18
18
|
* 400 "'<model>' does not support the `speed` parameter. ..."
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* ten minutes (`b5o = 600000`) and mints a new one afterwards; this journal keeps
|
|
8
8
|
* the same window.
|
|
9
9
|
*
|
|
10
|
-
* Rules (devlog/
|
|
10
|
+
* Rules (devlog/_fin/260923_claude_reset_grants/010_plan.md, "Ledger contract"):
|
|
11
11
|
* - Every read-modify-write runs synchronously inside a cross-process
|
|
12
12
|
* `BEGIN IMMEDIATE` lock on a sibling SQLite file; nothing awaits inside it,
|
|
13
13
|
* and the upstream claim always runs after the open record is on disk.
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { atomicWriteFile } from "../config/atomic-write";
|
|
4
|
+
import { getConfigDir } from "../config/paths";
|
|
5
|
+
import { readBoundedResponseBytes } from "../lib/bounded-body";
|
|
6
|
+
|
|
7
|
+
const SOURCE_URL = "https://models.dev/api.json";
|
|
8
|
+
const FILENAME = "azure-model-metadata-cache.json";
|
|
9
|
+
const TTL_MS = 24 * 60 * 60 * 1000;
|
|
10
|
+
|
|
11
|
+
export type AzureModelMetadata = { input?: string[]; contextWindow?: number; maxTokens?: number };
|
|
12
|
+
type Snapshot = { version: 1; fetchedAt: number; models: Record<string, AzureModelMetadata> };
|
|
13
|
+
type CacheState = { snapshot?: Snapshot | null; inFlight?: Promise<void>; retryAfter: number };
|
|
14
|
+
const cacheByConfigDir = new Map<string, CacheState>();
|
|
15
|
+
|
|
16
|
+
function cacheState(configDir: string): CacheState {
|
|
17
|
+
let state = cacheByConfigDir.get(configDir);
|
|
18
|
+
if (!state) {
|
|
19
|
+
state = { retryAfter: 0 };
|
|
20
|
+
cacheByConfigDir.set(configDir, state);
|
|
21
|
+
}
|
|
22
|
+
return state;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function isAzureModelMetadataDestination(baseUrl: string | undefined): boolean {
|
|
26
|
+
try {
|
|
27
|
+
return !!baseUrl && new URL(baseUrl).hostname.endsWith(".openai.azure.com");
|
|
28
|
+
} catch {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function positiveInteger(value: unknown): number | undefined {
|
|
34
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : undefined;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function record(value: unknown): Record<string, unknown> | undefined {
|
|
38
|
+
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
39
|
+
? value as Record<string, unknown> : undefined;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function parseModels(models: unknown, published: boolean): Record<string, AzureModelMetadata> {
|
|
43
|
+
const out: Record<string, AzureModelMetadata> = Object.create(null);
|
|
44
|
+
for (const [id, value] of Object.entries(record(models) ?? {}).slice(0, 2000)) {
|
|
45
|
+
const row = record(value);
|
|
46
|
+
if (!row || id.length > 256) continue;
|
|
47
|
+
const limit = published ? record(row.limit) : row;
|
|
48
|
+
const input = published ? record(row.modalities)?.input : row.input;
|
|
49
|
+
const modalities = Array.isArray(input)
|
|
50
|
+
? [...new Set(input.filter((item): item is string => item === "text" || item === "image" || item === "audio"))]
|
|
51
|
+
: [];
|
|
52
|
+
const contextWindow = positiveInteger(published ? limit?.context : limit?.contextWindow);
|
|
53
|
+
const maxTokens = positiveInteger(published ? limit?.output : limit?.maxTokens);
|
|
54
|
+
if (!modalities.length && contextWindow === undefined && maxTokens === undefined) continue;
|
|
55
|
+
out[id.toLowerCase()] = {
|
|
56
|
+
...(modalities.length ? { input: modalities } : {}),
|
|
57
|
+
...(contextWindow !== undefined ? { contextWindow } : {}),
|
|
58
|
+
...(maxTokens !== undefined ? { maxTokens } : {}),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function loadSnapshot(configDir: string, state: CacheState): Snapshot | null {
|
|
65
|
+
if (state.snapshot !== undefined) return state.snapshot;
|
|
66
|
+
state.snapshot = null;
|
|
67
|
+
try {
|
|
68
|
+
const raw = record(JSON.parse(readFileSync(join(configDir, FILENAME), "utf8")));
|
|
69
|
+
if (raw?.version === 1 && positiveInteger(raw.fetchedAt) !== undefined && record(raw.models)) {
|
|
70
|
+
state.snapshot = { version: 1, fetchedAt: raw.fetchedAt as number, models: parseModels(raw.models, false) };
|
|
71
|
+
}
|
|
72
|
+
} catch { /* Missing or corrupt optional metadata leaves bundled hints available. */ }
|
|
73
|
+
return state.snapshot;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Synchronous catalog fallback. Generation requests never fetch this snapshot. */
|
|
77
|
+
export function publishedAzureModelMetadata(modelId: string, configDir = getConfigDir()): AzureModelMetadata | undefined {
|
|
78
|
+
return loadSnapshot(configDir, cacheState(configDir))?.models[modelId.toLowerCase()];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Refresh once per day during discovery, coalesced and bounded; retain stale data offline. */
|
|
82
|
+
export async function refreshAzureModelMetadata(baseUrl: string | undefined, configDir = getConfigDir()): Promise<void> {
|
|
83
|
+
if (!isAzureModelMetadataDestination(baseUrl)) return;
|
|
84
|
+
// Capture state and destination together: tests and embedders can change config roots.
|
|
85
|
+
const state = cacheState(configDir);
|
|
86
|
+
const current = loadSnapshot(configDir, state);
|
|
87
|
+
if ((current && Date.now() - current.fetchedAt < TTL_MS) || Date.now() < state.retryAfter) return;
|
|
88
|
+
if (state.inFlight) return state.inFlight;
|
|
89
|
+
const path = join(configDir, FILENAME);
|
|
90
|
+
state.inFlight = (async () => {
|
|
91
|
+
try {
|
|
92
|
+
const signal = AbortSignal.timeout(2000);
|
|
93
|
+
const response = await fetch(SOURCE_URL, {
|
|
94
|
+
headers: { accept: "application/json" },
|
|
95
|
+
redirect: "error",
|
|
96
|
+
signal,
|
|
97
|
+
});
|
|
98
|
+
if (!response.ok) return;
|
|
99
|
+
const body = await readBoundedResponseBytes(response, { maxBytes: 16 * 1024 * 1024, signal });
|
|
100
|
+
if (body.oversized) return;
|
|
101
|
+
const raw = record(JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(body.bytes)));
|
|
102
|
+
// Provider-specific rows matter: Azure may expose text only where the direct vendor has vision.
|
|
103
|
+
const models = parseModels(record(raw?.azure)?.models, true);
|
|
104
|
+
if (!Object.keys(models).length) return;
|
|
105
|
+
state.snapshot = { version: 1, fetchedAt: Date.now(), models };
|
|
106
|
+
try { atomicWriteFile(path, JSON.stringify(state.snapshot) + "\n"); } catch { /* Memory still serves discovery. */ }
|
|
107
|
+
} catch { /* A public metadata outage must not fail provider discovery. */ }
|
|
108
|
+
finally { state.retryAfter = Date.now() + 60_000; }
|
|
109
|
+
})().finally(() => { state.inFlight = undefined; });
|
|
110
|
+
return state.inFlight;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function resetAzureModelMetadataForTests(): void {
|
|
114
|
+
cacheByConfigDir.clear();
|
|
115
|
+
}
|
package/src/providers/derive.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { CodexAccountMode, OcxProviderConfig } from "../types";
|
|
2
|
+
import { COST_VENDOR_PRIORITY, getModelMetadata, getModelMetadataCaseInsensitive } from "../generated/model-metadata";
|
|
2
3
|
import { cloneFastWire } from "./fastwire";
|
|
4
|
+
import { isAzureModelMetadataDestination, publishedAzureModelMetadata, type AzureModelMetadata } from "./azure-model-metadata";
|
|
3
5
|
import { resolveModelPolicy } from "./resolved-model-policy";
|
|
4
6
|
import {
|
|
5
7
|
PROVIDER_REGISTRY,
|
|
@@ -651,6 +653,25 @@ export function shouldCaseFoldMetadataModelId(providerId: string): boolean {
|
|
|
651
653
|
return entry?.metadataModelIdNormalize === "case-insensitive";
|
|
652
654
|
}
|
|
653
655
|
|
|
656
|
+
const AZURE_VENDOR_METADATA_PRIORITY = [
|
|
657
|
+
"azure-openai",
|
|
658
|
+
...COST_VENDOR_PRIORITY.filter(provider => provider !== "azure-openai"),
|
|
659
|
+
];
|
|
660
|
+
|
|
661
|
+
/**
|
|
662
|
+
* Fill missing Azure model metadata by destination and known model id. Custom provider
|
|
663
|
+
* names miss PROVIDER_ALIASES; arbitrary deployment aliases still need explicit metadata.
|
|
664
|
+
*/
|
|
665
|
+
export function azureVendorModelMetadata(baseUrl: string | undefined, modelId: string, metadataConfigDir?: string): AzureModelMetadata | undefined {
|
|
666
|
+
if (!isAzureModelMetadataDestination(baseUrl)) return undefined;
|
|
667
|
+
const published = publishedAzureModelMetadata(modelId, metadataConfigDir);
|
|
668
|
+
for (const provider of AZURE_VENDOR_METADATA_PRIORITY) {
|
|
669
|
+
const meta = getModelMetadata(provider, modelId) ?? getModelMetadataCaseInsensitive(provider, modelId);
|
|
670
|
+
if (meta) return { ...meta, ...published };
|
|
671
|
+
}
|
|
672
|
+
return published;
|
|
673
|
+
}
|
|
674
|
+
|
|
654
675
|
function entryToPreset(entry: ProviderRegistryEntry): DerivedProviderPreset {
|
|
655
676
|
return {
|
|
656
677
|
id: entry.id,
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* `devin-cli` used to be a local provider whose adapter spawned `devin acp`;
|
|
5
5
|
* it later became an account provider on the shared `devin` adapter, and the
|
|
6
6
|
* registry id itself has since merged into `devin` outright
|
|
7
|
-
* (devlog/
|
|
7
|
+
* (devlog/_fin/260913_devin_provider_merge).
|
|
8
8
|
*
|
|
9
9
|
* Only the adapter repair still lives here. The `authMode` half — rewriting
|
|
10
10
|
* `"local"` to `"oauth"` on the registry-id row — moved into
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Merge the retired `devin-cli` provider id into `devin` at startup
|
|
3
|
-
* (devlog/
|
|
3
|
+
* (devlog/_fin/260913_devin_provider_merge).
|
|
4
4
|
*
|
|
5
5
|
* The two registry entries were the same adapter, api-server, and credential
|
|
6
6
|
* shape — only the login path differed — so `devin-cli` was removed from the
|