@bitkyc08/opencodex 2.52.0-preview.20260911 → 2.52.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/gui/dist/assets/index-CWXut3rG.js +115 -0
- package/gui/dist/assets/index-EdoPnm9_.css +1 -0
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/devin.svg +49 -0
- package/gui/dist/provider-icons/omo.svg +42 -0
- package/package.json +3 -1
- package/src/AGENTS.md +1 -1
- package/src/adapters/cline-pass-deepseek-v4-tool-replay.ts +0 -1
- package/src/adapters/command-code.ts +0 -1
- package/src/adapters/cursor/checkpoint-store.ts +37 -0
- package/src/adapters/cursor/live-transport.ts +74 -2
- package/src/adapters/cursor.ts +6 -0
- package/src/adapters/devin/cloud-direct/auth.ts +264 -0
- package/src/adapters/devin/cloud-direct/catalog.ts +306 -0
- package/src/adapters/devin/cloud-direct/chat.ts +1274 -0
- package/src/adapters/devin/cloud-direct/index.ts +65 -0
- package/src/adapters/devin/cloud-direct/metadata.ts +134 -0
- package/src/adapters/devin/cloud-direct/wire.ts +206 -0
- package/src/adapters/devin/live-models.ts +133 -0
- package/src/adapters/devin-cli/acp.ts +204 -0
- package/src/adapters/devin-cli/adapter.ts +345 -0
- package/src/adapters/devin-cli/binary.ts +69 -0
- package/src/adapters/devin-cli/models.ts +57 -0
- package/src/adapters/devin.ts +326 -0
- package/src/adapters/google.ts +1 -1
- package/src/adapters/openai-chat.ts +31 -10
- package/src/adapters/openai-responses.ts +16 -1
- package/src/adapters/registry.ts +25 -1
- package/src/bridge.ts +8 -2
- package/src/claude/inbound-cache-stabilize.ts +130 -0
- package/src/claude/inbound.ts +45 -5
- package/src/cli/account-auth.ts +60 -1
- package/src/cli/account-extended.ts +23 -30
- package/src/cli/account.ts +2 -1
- package/src/cli/capabilities.ts +50 -8
- package/src/cli/config-command.ts +2 -2
- package/src/cli/dispatch.ts +14 -2
- package/src/cli/export-command.ts +11 -25
- package/src/cli/help.ts +2 -2
- package/src/cli/opencode.ts +5 -0
- package/src/cli/registry.ts +15 -4
- package/src/clients/config-export/cline.ts +71 -0
- package/src/clients/config-export/contracts.ts +10 -1
- package/src/clients/config-export/model-metadata.ts +33 -0
- package/src/clients/config-export/zcode.ts +23 -12
- package/src/clients/config-export.ts +156 -4
- package/src/codex/account-store.ts +7 -2
- package/src/codex/auth-context.ts +1 -1
- package/src/codex/catalog/effort.ts +4 -3
- package/src/codex/catalog/metadata.ts +8 -4
- package/src/codex/catalog/native-models.ts +24 -4
- package/src/codex/catalog/parsing.ts +19 -1
- package/src/codex/catalog/provider-fetch.ts +57 -0
- package/src/codex/catalog/sync.ts +1 -1
- package/src/codex/context-compat.ts +97 -0
- package/src/codex/context-owner.ts +201 -0
- package/src/codex/history-provider.ts +161 -14
- package/src/codex/inject-coordination.ts +3 -2
- package/src/codex/inject.ts +128 -9
- package/src/codex/pool-rotation.ts +8 -292
- package/src/codex/quota.ts +41 -12
- package/src/codex/retired-model-migration.ts +41 -0
- package/src/codex/routing.ts +327 -22
- package/src/codex/warmup.ts +2 -2
- package/src/combos/failover.ts +5 -0
- package/src/combos/resolve.ts +11 -15
- package/src/config.ts +46 -14
- package/src/generated/compatibility-version.json +293 -113
- package/src/grok/grpc-web.ts +120 -0
- package/src/grok/reset-coupon-ledger.ts +139 -0
- package/src/grok/reset-coupons.ts +278 -0
- package/src/integrations/catalog-refresh.ts +1 -1
- package/src/integrations/cline-document.ts +73 -0
- package/src/integrations/cline-io.ts +149 -0
- package/src/integrations/cline-transaction.ts +42 -0
- package/src/integrations/config-io.ts +21 -2
- package/src/integrations/journal.ts +43 -1
- package/src/integrations/omp-yaml-source.ts +123 -4
- package/src/integrations/ownership-policy.ts +7 -0
- package/src/integrations/ownership.ts +36 -0
- package/src/integrations/registry.ts +27 -0
- package/src/integrations/state.ts +5 -2
- package/src/integrations/store.ts +6 -0
- package/src/integrations/writer.ts +53 -9
- package/src/lab/subject/behavior-fingerprint.ts +1 -1
- package/src/lib/abort.ts +36 -0
- package/src/lib/local-destinations.ts +1 -1
- package/src/lib/upstream-retry.ts +36 -1
- package/src/oauth/account-quota-rank.ts +11 -0
- package/src/oauth/callback-server.ts +10 -5
- package/src/oauth/devin/api-base.ts +63 -0
- package/src/oauth/devin/login.ts +1 -0
- package/src/oauth/devin/register-user.ts +186 -0
- package/src/oauth/devin/types.ts +71 -0
- package/src/oauth/devin-cli.ts +149 -0
- package/src/oauth/devin.ts +170 -0
- package/src/oauth/generic-account-failover.ts +169 -1
- package/src/oauth/index.ts +20 -1
- package/src/oauth/login-cli.ts +19 -5
- package/src/oauth/pool-kernel.ts +321 -0
- package/src/oauth/pool-settings-capability.ts +127 -9
- package/src/oauth/store.ts +7 -3
- package/src/oauth/token-guardian.ts +1 -1
- package/src/providers/codebuddy-models.ts +0 -3
- package/src/providers/command-code-efforts.ts +23 -4
- package/src/providers/default-aliases.ts +4 -0
- package/src/providers/derive.ts +8 -0
- package/src/providers/devin-cli-authmode-migration.ts +57 -0
- package/src/providers/key-failover.ts +175 -0
- package/src/providers/model-rename-startup.ts +21 -1
- package/src/providers/qoder-models.ts +0 -1
- package/src/providers/quota-key-accounts.ts +50 -0
- package/src/providers/quota-routing-cache.ts +65 -6
- package/src/providers/quota-types.ts +7 -0
- package/src/providers/quota.ts +113 -30
- package/src/providers/registry.ts +237 -77
- package/src/providers/stale-context-window-migration.ts +92 -0
- package/src/providers/zai-responses-migration.ts +45 -0
- package/src/quota/reset-observer.ts +2 -1
- package/src/quota/reset-seen-store.ts +9 -1
- package/src/remote-control/crypto.ts +442 -0
- package/src/remote-control/host.ts +175 -0
- package/src/remote-control/index.ts +100 -0
- package/src/remote-control/protocol.ts +200 -0
- package/src/remote-control/relay.ts +162 -0
- package/src/remote-control/workspace-agent-protocol.ts +246 -0
- package/src/remote-control/workspace-rpc-framing.ts +128 -0
- package/src/remote-control/workspace-tools.ts +237 -0
- package/src/remote-control/workspace-utf8.ts +24 -0
- package/src/responses/custom-tool-compat.ts +23 -0
- package/src/router.ts +6 -1
- package/src/routing/compatibility/behavior.ts +3 -0
- package/src/server/adapter-resolve.ts +6 -2
- package/src/server/auth-cors.ts +71 -6
- package/src/server/chat-completions.ts +5 -3
- package/src/server/chat-native.ts +9 -0
- package/src/server/claude-messages.ts +25 -30
- package/src/server/context-history.ts +207 -0
- package/src/server/images.ts +28 -1
- package/src/server/index.ts +42 -22
- package/src/server/live.ts +2 -1
- package/src/server/management/agent-settings-routes.ts +7 -1
- package/src/server/management/config-routes.ts +3 -3
- package/src/server/management/grok-coupon-routes.ts +287 -0
- package/src/server/management/integration-routes.ts +6 -1
- package/src/server/management/oauth-account-routes.ts +124 -7
- package/src/server/management/provider-routes.ts +77 -2
- package/src/server/management/route-registry.ts +6 -1
- package/src/server/management-api.ts +7 -0
- package/src/server/request-log.ts +4 -0
- package/src/server/responses/codex-ws-exchange.ts +90 -14
- package/src/server/responses/codex-ws-wire.ts +51 -1
- package/src/server/responses/compact.ts +8 -1
- package/src/server/responses/core.ts +188 -22
- package/src/server/responses/ws-upstream.ts +1 -1
- package/src/server/responses-undeclared-tool-guard.ts +261 -12
- package/src/server/zai-responses-startup.ts +21 -0
- package/src/types/config.ts +33 -3
- package/src/types/provider.ts +54 -4
- package/src/types/request.ts +1 -1
- package/src/types/tools.ts +36 -6
- package/src/update/job.ts +33 -11
- package/src/vision/plan.ts +40 -37
- package/src/web-search/index.ts +1 -0
- package/gui/dist/assets/index-BoBRSehJ.css +0 -1
- package/gui/dist/assets/index-Dx0xv2EA.js +0 -115
package/src/server/index.ts
CHANGED
|
@@ -24,6 +24,7 @@ import { grokDefaultReasoningEffort } from "../grok/effort";
|
|
|
24
24
|
import { flushConfigDirHardening } from "../config/paths";
|
|
25
25
|
import { migrateStartupSubagentModels } from "./subagent-models-startup";
|
|
26
26
|
import { migrateStartupXaiResponses } from "./xai-responses-startup";
|
|
27
|
+
import { migrateStartupZaiResponses } from "./zai-responses-startup";
|
|
27
28
|
import { reconcileOAuthProviders } from "../oauth";
|
|
28
29
|
import { withCatalogWriteSerialization } from "../codex/catalog-write-serialization";
|
|
29
30
|
import { invalidateCodexModelsCacheWithPermit } from "../codex/catalog/sync";
|
|
@@ -192,6 +193,7 @@ import { anthropicErrorResponse } from "../claude/outbound";
|
|
|
192
193
|
import { buildDesktop3pRegistry, generateDesktop3pModels } from "../claude/desktop-3p";
|
|
193
194
|
import { buildDesktopDiscoveryInputs } from "../claude/desktop-discovery-inputs";
|
|
194
195
|
import { runClaudeAuthModeMigration } from "../claude/auth-mode-migration";
|
|
196
|
+
import { runRetiredCodexModelMigration } from "../codex/retired-model-migration";
|
|
195
197
|
import {
|
|
196
198
|
bindNativeMainStartupLifecycle,
|
|
197
199
|
blockNativeMainStartupForUnownedServiceHome,
|
|
@@ -203,6 +205,8 @@ import {
|
|
|
203
205
|
import { handleImages } from "./images";
|
|
204
206
|
import { handleLive, logLiveSidebandFrame, parseLiveSidebandTarget, resolveLiveSidebandUpgrade } from "./live";
|
|
205
207
|
import { handleSearch } from "./search";
|
|
208
|
+
import { handleContextHistory } from "./context-history";
|
|
209
|
+
import { codexCompatibleUrl, contextEndpoint, contextRelayActivated } from "../codex/context-compat";
|
|
206
210
|
import { fetchAllModels, handleManagementAPI, VERSION, type ManagementApiDeps } from "./management-api";
|
|
207
211
|
import {
|
|
208
212
|
createManagementSessionControl,
|
|
@@ -666,7 +670,7 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
666
670
|
// Reconcile disk-backed presets first: it replaces provider rows and must not undo
|
|
667
671
|
// an in-memory wire upgrade when that upgrade's persistence is temporarily unavailable.
|
|
668
672
|
reconcileOAuthProviders(startupConfig);
|
|
669
|
-
const config = migrateStartupXaiResponses(startupConfig);
|
|
673
|
+
const config = migrateStartupZaiResponses(migrateStartupXaiResponses(startupConfig));
|
|
670
674
|
warnAgentTaskRecoveryStartup(config);
|
|
671
675
|
setLiveStateStoreConfig(config);
|
|
672
676
|
applyProxyEnv(config);
|
|
@@ -682,24 +686,11 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
682
686
|
// indistinguishable from "never chose". Pin those to subscription once so an upgrade
|
|
683
687
|
// never silently moves a deliberate subscriber onto proxy.
|
|
684
688
|
if (runClaudeAuthModeMigration(config)) saveConfig(config);
|
|
685
|
-
//
|
|
686
|
-
//
|
|
687
|
-
//
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
if (Date.now() >= SIDECAR_MIGRATION_CUTOFF) {
|
|
691
|
-
let migrated = false;
|
|
692
|
-
if (config.webSearchSidecar?.model === "gpt-5.4-mini") {
|
|
693
|
-
config.webSearchSidecar = { ...config.webSearchSidecar, model: "gpt-5.6-luna" };
|
|
694
|
-
migrated = true;
|
|
695
|
-
}
|
|
696
|
-
if (config.visionSidecar?.model === "gpt-5.4-mini") {
|
|
697
|
-
config.visionSidecar = { ...config.visionSidecar, model: "gpt-5.6-luna" };
|
|
698
|
-
migrated = true;
|
|
699
|
-
}
|
|
700
|
-
if (migrated) saveConfig(config);
|
|
701
|
-
}
|
|
702
|
-
}
|
|
689
|
+
// Retired Codex-login models: a stored gpt-5.4-mini is a guaranteed 404 for the search and
|
|
690
|
+
// vision sidecars and for pool warmup, so it moves to gpt-5.6-luna. Extracted so the rule is
|
|
691
|
+
// testable on its own; see src/codex/retired-model-migration.ts for why exact equality also
|
|
692
|
+
// rewrites an explicit choice.
|
|
693
|
+
if (runRetiredCodexModelMigration(config)) saveConfig(config);
|
|
703
694
|
// Resolve unattended service-home authority before any Codex lock, cache, owner,
|
|
704
695
|
// journal, or credential path. Both positive foreign evidence and an unprovable
|
|
705
696
|
// ownership state are non-authority.
|
|
@@ -856,6 +847,7 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
856
847
|
if (path === "/v1/messages" || path === "/v1/chat/completions") return req.method === "POST";
|
|
857
848
|
if (path === "/v1/messages/count_tokens") return req.method === "POST";
|
|
858
849
|
if (path === "/v1/alpha/search") return req.method === "POST";
|
|
850
|
+
if (contextEndpoint(path)) return req.method === "POST";
|
|
859
851
|
if (path === "/v1/images/generations" || path === "/v1/images/edits") {
|
|
860
852
|
return req.method === "POST";
|
|
861
853
|
}
|
|
@@ -1102,7 +1094,7 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
1102
1094
|
// The unauthenticated loopback listener (#1102) serves a fixed allowlist and nothing
|
|
1103
1095
|
// else. Rejecting here, before any handler runs, is what keeps the surface from growing
|
|
1104
1096
|
// silently when a route is added below.
|
|
1105
|
-
if (ingress === "unauthenticated-loopback" && !loopbackRouteAllowed(
|
|
1097
|
+
if (ingress === "unauthenticated-loopback" && !loopbackRouteAllowed(codexCompatibleUrl(req.url), req)) {
|
|
1106
1098
|
return withCors(
|
|
1107
1099
|
formatErrorResponse(404, "not_found", `Unknown endpoint: ${req.method} ${new URL(req.url).pathname}`),
|
|
1108
1100
|
req,
|
|
@@ -1111,7 +1103,7 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
1111
1103
|
}
|
|
1112
1104
|
// Tailscale Serve terminates only on this separately bound loopback socket. Reject before
|
|
1113
1105
|
// dispatch so no data, readiness, health, WebSocket, or unknown-static handler can run.
|
|
1114
|
-
if (ingress === "hub-management" && !managementIngressRouteAllowed(
|
|
1106
|
+
if (ingress === "hub-management" && !managementIngressRouteAllowed(codexCompatibleUrl(req.url), req)) {
|
|
1115
1107
|
return withCors(
|
|
1116
1108
|
formatErrorResponse(404, "not_found", `Unknown endpoint: ${req.method} ${new URL(req.url).pathname}`),
|
|
1117
1109
|
req,
|
|
@@ -1124,7 +1116,7 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
1124
1116
|
// same code path a plain loopback bind has always taken — Host-header check included.
|
|
1125
1117
|
// Routing, provider selection and response bodies keep using `config`.
|
|
1126
1118
|
const policy: RequestPolicyView = ingress === "unauthenticated-loopback" ? loopbackPolicy() : config;
|
|
1127
|
-
const url =
|
|
1119
|
+
const url = codexCompatibleUrl(req.url);
|
|
1128
1120
|
markActivity(`${req.method} ${url.pathname}`);
|
|
1129
1121
|
|
|
1130
1122
|
// Readiness is exact-GET on the literal /readyz path. Compare the DECODED
|
|
@@ -1966,6 +1958,34 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
1966
1958
|
}), req, policy);
|
|
1967
1959
|
}
|
|
1968
1960
|
|
|
1961
|
+
if (contextEndpoint(url.pathname) !== undefined && req.method === "POST" && contextRelayActivated()) {
|
|
1962
|
+
// No timeout disable here. The relay is a bounded JSON round trip that owns one deadline
|
|
1963
|
+
// from entry; removing the idle timeout first would let an unfinished body hold an
|
|
1964
|
+
// admitted turn slot indefinitely, before that deadline ever starts.
|
|
1965
|
+
if (isDraining()) {
|
|
1966
|
+
return drainingResponse(req, policy);
|
|
1967
|
+
}
|
|
1968
|
+
const admission = resolveApiAuth(req, policy);
|
|
1969
|
+
if (!admission) return withCors(formatErrorResponse(401, "authentication_error", "opencodex API key required"), req, policy);
|
|
1970
|
+
if (!isAllowedRequestOrigin(req, policy)) {
|
|
1971
|
+
return withCors(formatErrorResponse(403, "origin_rejected", "cross-origin data-plane request blocked"), req, policy);
|
|
1972
|
+
}
|
|
1973
|
+
const start = Date.now();
|
|
1974
|
+
const requestId = nextRequestLogId(start);
|
|
1975
|
+
const logCtx: RequestLogContext = {
|
|
1976
|
+
model: "context_history",
|
|
1977
|
+
provider: "unknown",
|
|
1978
|
+
...admissionFields(admission),
|
|
1979
|
+
};
|
|
1980
|
+
return runAdmittedHttpTurn(req, policy, async turnAdmissionLease => {
|
|
1981
|
+
const response = await handleContextHistory(req, config, logCtx, contextEndpoint(url.pathname)!,
|
|
1982
|
+
turnAdmissionLease, admission, () => resolveApiAuth(req, policy));
|
|
1983
|
+
addFinalRequestLog(requestId, start, logCtx, response.status,
|
|
1984
|
+
response.status === 499 ? { closeReason: "client_cancel" } : undefined);
|
|
1985
|
+
return withCors(response, req, policy);
|
|
1986
|
+
});
|
|
1987
|
+
}
|
|
1988
|
+
|
|
1969
1989
|
if (url.pathname === "/v1/alpha/search" && req.method === "POST") {
|
|
1970
1990
|
disableResponsesRequestTimeout(req, requestServer);
|
|
1971
1991
|
if (isDraining()) {
|
package/src/server/live.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { codexCompatibleUrl } from "../codex/context-compat";
|
|
1
2
|
/**
|
|
2
3
|
* /v1/live and /v1/realtime/calls relay (issue #371).
|
|
3
4
|
*
|
|
@@ -635,7 +636,7 @@ export async function handleLive(
|
|
|
635
636
|
// Frameless API-shape call-create posts to `{base}/live` without the AVAS
|
|
636
637
|
// query (openai/codex RealtimeCallClient, realtime_call.rs); only the
|
|
637
638
|
// realtime/calls inbound shape keeps the legacy keyed AVAS endpoint.
|
|
638
|
-
url =
|
|
639
|
+
url = codexCompatibleUrl(req.url).pathname === "/v1/live"
|
|
639
640
|
? forwardLiveUrl(relay.providerBaseUrl, /* usesBackendShape */ false)
|
|
640
641
|
: keyedLiveUrl(relay.providerBaseUrl);
|
|
641
642
|
}
|
|
@@ -948,10 +948,16 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
948
948
|
const { parseDesktopProfile, reconcileDesktopProfile } = await import("../../claude/desktop-profile");
|
|
949
949
|
const parsed = parseDesktopProfile(body.profile);
|
|
950
950
|
const current = await buildClaudeDesktopState(config);
|
|
951
|
+
const availableRoutes = new Set(current.models.filter(item => item.available).map(item => item.route));
|
|
952
|
+
for (const route of Object.keys(parsed.assignments)) {
|
|
953
|
+
if (!current.profile.assignments[route] && !availableRoutes.has(route)) {
|
|
954
|
+
throw new Error(`현재 사용할 수 없는 모델은 추가할 수 없습니다: ${route}`);
|
|
955
|
+
}
|
|
956
|
+
}
|
|
951
957
|
for (const model of current.models.filter(item => !item.available)) {
|
|
952
958
|
const before = current.profile.assignments[model.route];
|
|
953
959
|
const after = parsed.assignments[model.route];
|
|
954
|
-
if (JSON.stringify(before) !== JSON.stringify(after)) {
|
|
960
|
+
if (after !== undefined && JSON.stringify(before) !== JSON.stringify(after)) {
|
|
955
961
|
throw new Error(`현재 사용할 수 없는 모델은 옮길 수 없습니다: ${model.route}`);
|
|
956
962
|
}
|
|
957
963
|
}
|
|
@@ -237,7 +237,7 @@ export async function syncEnabledClientIntegrations(
|
|
|
237
237
|
},
|
|
238
238
|
config,
|
|
239
239
|
port,
|
|
240
|
-
}, ["mcode", "pi", "aside", "raycast"]));
|
|
240
|
+
}, ["mcode", "pi", "aside", "raycast", "omo", "cline"]));
|
|
241
241
|
|
|
242
242
|
return out;
|
|
243
243
|
}
|
|
@@ -820,8 +820,8 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
820
820
|
if (body.vision && (body.vision.model !== undefined || body.vision.reasoning !== undefined)) {
|
|
821
821
|
visionReasoningTouched = true;
|
|
822
822
|
const model = typeof body.vision.model === "string"
|
|
823
|
-
? (body.vision.model === "" ? "gpt-5.
|
|
824
|
-
: (config.visionSidecar?.model || "gpt-5.
|
|
823
|
+
? (body.vision.model === "" ? "gpt-5.6-luna" : body.vision.model)
|
|
824
|
+
: (config.visionSidecar?.model || "gpt-5.6-luna");
|
|
825
825
|
const sourceReasoning = body.vision.reasoning ?? config.visionSidecar?.reasoning;
|
|
826
826
|
normalizedVisionReasoning = sourceReasoning === undefined
|
|
827
827
|
? undefined
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Management API handlers for Grok quota reset coupons.
|
|
3
|
+
*
|
|
4
|
+
* Exposes inspection and consumption of Grok billing reset coupons via gRPC-Web
|
|
5
|
+
* to Grok ConsumerUiSvc upstream endpoints.
|
|
6
|
+
*
|
|
7
|
+
* Inherits management authentication from requireManagementAuth in management-api.ts.
|
|
8
|
+
* Lazy-loaded by handleGrokCouponRoutesOnDemand to keep startup fast and honor the
|
|
9
|
+
* core-lab boundary contract.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { jsonResponse } from "../auth-cors";
|
|
13
|
+
import type { ManagementContext } from "./context";
|
|
14
|
+
import { isCodexResetCreditOperationId } from "../../codex/reset-credit-recovery";
|
|
15
|
+
import { getValidAccessSnapshotForAccount } from "../../oauth";
|
|
16
|
+
import { listAccounts, captureOAuthAccountSelection } from "../../oauth/store";
|
|
17
|
+
import {
|
|
18
|
+
getGrokRemainingResets,
|
|
19
|
+
redeemGrokResetCoupon,
|
|
20
|
+
type GrokResetCoupon,
|
|
21
|
+
} from "../../grok/reset-coupons";
|
|
22
|
+
import {
|
|
23
|
+
openGrokResetCouponOperation,
|
|
24
|
+
recordGrokResetCouponSettlement,
|
|
25
|
+
type GrokResetCouponOperationRecord,
|
|
26
|
+
} from "../../grok/reset-coupon-ledger";
|
|
27
|
+
|
|
28
|
+
export interface GrokResetCouponsResponse {
|
|
29
|
+
accountId: string;
|
|
30
|
+
tokens: Array<{
|
|
31
|
+
tokenId: string;
|
|
32
|
+
validityStart: string;
|
|
33
|
+
validityEnd: string;
|
|
34
|
+
}>;
|
|
35
|
+
remaining: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface GrokConsumeCouponRequestBody {
|
|
39
|
+
accountId?: string;
|
|
40
|
+
tokenId?: string;
|
|
41
|
+
operationId?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function resolveTargetAccountId(requestedAccountId?: string): string {
|
|
45
|
+
if (requestedAccountId && requestedAccountId.trim() !== "") {
|
|
46
|
+
return requestedAccountId.trim();
|
|
47
|
+
}
|
|
48
|
+
const selection = captureOAuthAccountSelection("xai");
|
|
49
|
+
if (selection?.accountId) {
|
|
50
|
+
return selection.accountId;
|
|
51
|
+
}
|
|
52
|
+
const accounts = listAccounts("xai");
|
|
53
|
+
if (accounts.length > 0) {
|
|
54
|
+
return accounts[0].id;
|
|
55
|
+
}
|
|
56
|
+
throw new Error("No xAI account found or active");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function handleGrokCouponRoutes(ctx: ManagementContext): Promise<Response | null> {
|
|
60
|
+
const { url, req, config } = ctx;
|
|
61
|
+
const { pathname } = url;
|
|
62
|
+
|
|
63
|
+
if (pathname === "/api/grok/reset-coupons") {
|
|
64
|
+
if (req.method !== "GET") {
|
|
65
|
+
return jsonResponse({ error: "Method not allowed" }, 405, req, config);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const queryAccountId = url.searchParams.get("accountId") ?? undefined;
|
|
69
|
+
let accountId: string;
|
|
70
|
+
try {
|
|
71
|
+
accountId = resolveTargetAccountId(queryAccountId);
|
|
72
|
+
} catch (err) {
|
|
73
|
+
return jsonResponse(
|
|
74
|
+
{ error: { code: "no_account", message: err instanceof Error ? err.message : String(err) } },
|
|
75
|
+
400,
|
|
76
|
+
req,
|
|
77
|
+
config,
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
let tokenSnapshot;
|
|
82
|
+
try {
|
|
83
|
+
tokenSnapshot = await getValidAccessSnapshotForAccount("xai", accountId, { requireUsableAccount: true });
|
|
84
|
+
} catch (err) {
|
|
85
|
+
return jsonResponse(
|
|
86
|
+
{ error: { code: "auth_failed", message: "Failed to resolve valid xAI credentials for account" } },
|
|
87
|
+
401,
|
|
88
|
+
req,
|
|
89
|
+
config,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
const remainingResult = await getGrokRemainingResets({
|
|
95
|
+
accessToken: tokenSnapshot.accessToken,
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const payload: GrokResetCouponsResponse = {
|
|
99
|
+
accountId,
|
|
100
|
+
tokens: remainingResult.tokens.map((t) => ({
|
|
101
|
+
tokenId: t.tokenId,
|
|
102
|
+
validityStart: t.validityStart,
|
|
103
|
+
validityEnd: t.validityEnd,
|
|
104
|
+
})),
|
|
105
|
+
remaining: remainingResult.tokens.length,
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
return jsonResponse(payload, 200, req, config);
|
|
109
|
+
} catch (err) {
|
|
110
|
+
return jsonResponse(
|
|
111
|
+
{ error: { code: "upstream_error", message: err instanceof Error ? err.message : String(err) } },
|
|
112
|
+
502,
|
|
113
|
+
req,
|
|
114
|
+
config,
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (pathname === "/api/grok/reset-coupons/consume") {
|
|
120
|
+
if (req.method !== "POST") {
|
|
121
|
+
return jsonResponse({ error: "Method not allowed" }, 405, req, config);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
let body: GrokConsumeCouponRequestBody;
|
|
125
|
+
try {
|
|
126
|
+
body = (await req.json()) as GrokConsumeCouponRequestBody;
|
|
127
|
+
} catch {
|
|
128
|
+
return jsonResponse({ error: { code: "invalid_json", message: "Invalid JSON body" } }, 400, req, config);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const { accountId: rawAccountId, tokenId: requestedTokenId, operationId } = body;
|
|
132
|
+
|
|
133
|
+
if (operationId !== undefined && !isCodexResetCreditOperationId(operationId)) {
|
|
134
|
+
return jsonResponse(
|
|
135
|
+
{ error: { code: "invalid_operation_id", message: "operationId must be a valid UUIDv4" } },
|
|
136
|
+
400,
|
|
137
|
+
req,
|
|
138
|
+
config,
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
let accountId: string;
|
|
143
|
+
try {
|
|
144
|
+
accountId = resolveTargetAccountId(rawAccountId);
|
|
145
|
+
} catch (err) {
|
|
146
|
+
return jsonResponse(
|
|
147
|
+
{ error: { code: "no_account", message: err instanceof Error ? err.message : String(err) } },
|
|
148
|
+
400,
|
|
149
|
+
req,
|
|
150
|
+
config,
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
let tokenSnapshot;
|
|
155
|
+
try {
|
|
156
|
+
tokenSnapshot = await getValidAccessSnapshotForAccount("xai", accountId, { requireUsableAccount: true });
|
|
157
|
+
} catch (err) {
|
|
158
|
+
return jsonResponse(
|
|
159
|
+
{ error: { code: "auth_failed", message: "Failed to resolve valid xAI credentials for account" } },
|
|
160
|
+
401,
|
|
161
|
+
req,
|
|
162
|
+
config,
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Journaling and Idempotency settlement check
|
|
167
|
+
const effectiveOpId = operationId ?? crypto.randomUUID();
|
|
168
|
+
const opRecord = openGrokResetCouponOperation({
|
|
169
|
+
accountId,
|
|
170
|
+
tokenId: requestedTokenId,
|
|
171
|
+
operationId: effectiveOpId,
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
if (opRecord.kind === "replay") {
|
|
175
|
+
return jsonResponse(
|
|
176
|
+
{
|
|
177
|
+
code: opRecord.code,
|
|
178
|
+
replayed: true,
|
|
179
|
+
tokenId: opRecord.tokenId,
|
|
180
|
+
settledAt: opRecord.settledAt,
|
|
181
|
+
},
|
|
182
|
+
200,
|
|
183
|
+
req,
|
|
184
|
+
config,
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (opRecord.kind === "identity-mismatch") {
|
|
189
|
+
return jsonResponse(
|
|
190
|
+
{
|
|
191
|
+
error: {
|
|
192
|
+
code: "operation_id_owned_by_another_account",
|
|
193
|
+
message: "Operation ID was previously registered with a different account or token",
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
409,
|
|
197
|
+
req,
|
|
198
|
+
config,
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (opRecord.kind !== "execute") {
|
|
203
|
+
return jsonResponse(
|
|
204
|
+
{
|
|
205
|
+
error: {
|
|
206
|
+
code: opRecord.kind,
|
|
207
|
+
message: "Coupon ledger capacity or unavailable failure",
|
|
208
|
+
},
|
|
209
|
+
},
|
|
210
|
+
503,
|
|
211
|
+
req,
|
|
212
|
+
config,
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
let resolvedTokenId = requestedTokenId;
|
|
217
|
+
if (!resolvedTokenId) {
|
|
218
|
+
try {
|
|
219
|
+
const remaining = await getGrokRemainingResets({ accessToken: tokenSnapshot.accessToken });
|
|
220
|
+
if (!remaining.tokens || remaining.tokens.length === 0) {
|
|
221
|
+
recordGrokResetCouponSettlement({
|
|
222
|
+
operationId: effectiveOpId,
|
|
223
|
+
code: "no_coupons_available",
|
|
224
|
+
status: "failed",
|
|
225
|
+
});
|
|
226
|
+
return jsonResponse(
|
|
227
|
+
{ error: { code: "no_coupons_available", message: "No reset coupons available to redeem" } },
|
|
228
|
+
400,
|
|
229
|
+
req,
|
|
230
|
+
config,
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
resolvedTokenId = remaining.tokens[0].tokenId;
|
|
234
|
+
} catch (err) {
|
|
235
|
+
return jsonResponse(
|
|
236
|
+
{ error: { code: "fetch_resets_failed", message: err instanceof Error ? err.message : String(err) } },
|
|
237
|
+
502,
|
|
238
|
+
req,
|
|
239
|
+
config,
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
try {
|
|
245
|
+
const redeemResult = await redeemGrokResetCoupon({
|
|
246
|
+
accessToken: tokenSnapshot.accessToken,
|
|
247
|
+
tokenId: resolvedTokenId,
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
recordGrokResetCouponSettlement({
|
|
251
|
+
operationId: effectiveOpId,
|
|
252
|
+
tokenId: resolvedTokenId,
|
|
253
|
+
code: "redeemed",
|
|
254
|
+
status: "success",
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
return jsonResponse(
|
|
258
|
+
{
|
|
259
|
+
success: true,
|
|
260
|
+
code: "redeemed",
|
|
261
|
+
replayed: false,
|
|
262
|
+
tokenId: resolvedTokenId,
|
|
263
|
+
accountId,
|
|
264
|
+
operationId: effectiveOpId,
|
|
265
|
+
},
|
|
266
|
+
200,
|
|
267
|
+
req,
|
|
268
|
+
config,
|
|
269
|
+
);
|
|
270
|
+
} catch (err) {
|
|
271
|
+
recordGrokResetCouponSettlement({
|
|
272
|
+
operationId: effectiveOpId,
|
|
273
|
+
tokenId: resolvedTokenId,
|
|
274
|
+
code: "redeem_failed",
|
|
275
|
+
status: "failed",
|
|
276
|
+
});
|
|
277
|
+
return jsonResponse(
|
|
278
|
+
{ error: { code: "redeem_failed", message: err instanceof Error ? err.message : String(err) } },
|
|
279
|
+
502,
|
|
280
|
+
req,
|
|
281
|
+
config,
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
asideJournalDeleteResponse, type AsideProfileRouteOptions,
|
|
17
17
|
} from "./aside-profile-routes";
|
|
18
18
|
import type { IntegrationIO } from "../../integrations/config-io";
|
|
19
|
+
import { createClineIO } from "../../integrations/cline-io";
|
|
19
20
|
import { matchesOperationResult } from "../../integrations/journal";
|
|
20
21
|
import {
|
|
21
22
|
INTEGRATION_CLIENT_IDS,
|
|
@@ -534,7 +535,11 @@ export async function handleIntegrationRoutes(ctx: ManagementContext): Promise<R
|
|
|
534
535
|
undoable: (() => {
|
|
535
536
|
if (snapshot === "expired") return false;
|
|
536
537
|
if (newestByClient.get(operation.clientId) !== operation.opId) return false;
|
|
537
|
-
const current =
|
|
538
|
+
const current = operation.clientId === "cline" ? (() => {
|
|
539
|
+
const io = createClineIO(integrationMutationTestHooks?.io ?? store.io(), operation.configPath, store);
|
|
540
|
+
const read = io.readText(operation.configPath);
|
|
541
|
+
return read.kind === "text" ? read.text : read.kind === "missing" ? null : undefined;
|
|
542
|
+
})() : currentConfigText(operation.configPath);
|
|
538
543
|
return current === undefined ? false : matchesOperationResult(operation, current);
|
|
539
544
|
})(),
|
|
540
545
|
/*
|