codex-multi-auth 2.19.1 → 2.21.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/.codex-plugin/plugin.json +9 -9
- package/LICENSE +21 -21
- package/README.md +149 -148
- package/assets/codex-multi-auth-icon.svg +39 -39
- package/assets/readme-hero.svg +31 -31
- package/config/README.md +31 -31
- package/config/codex-legacy.json +1021 -1021
- package/config/codex-modern.json +347 -347
- package/config/minimal-codex.json +15 -15
- package/dist/index.js +1 -1
- package/dist/lib/accounts/rate-limits.js +5 -0
- package/dist/lib/accounts.js +5 -4
- package/dist/lib/auth/account-access.js +2 -2
- package/dist/lib/codex-cli/version.d.ts +22 -0
- package/dist/lib/codex-cli/version.js +78 -0
- package/dist/lib/codex-manager/commands/limits.d.ts +2 -0
- package/dist/lib/codex-manager/commands/limits.js +31 -0
- package/dist/lib/codex-manager/commands/status.js +1 -1
- package/dist/lib/codex-manager.js +2 -0
- package/dist/lib/constants.d.ts +2 -0
- package/dist/lib/constants.js +2 -0
- package/dist/lib/context-budget/model-context-windows.d.ts +1 -1
- package/dist/lib/context-budget/model-context-windows.js +4 -0
- package/dist/lib/context-budget-response.js +8 -8
- package/dist/lib/context-overflow.js +10 -10
- package/dist/lib/luna-reserve.d.ts +26 -0
- package/dist/lib/luna-reserve.js +68 -0
- package/dist/lib/oauth-success.html +125 -125
- package/dist/lib/policy/runtime-policy.js +14 -0
- package/dist/lib/prompts/codex-host-bridge.js +138 -138
- package/dist/lib/prompts/codex.js +79 -79
- package/dist/lib/request/helpers/model-map.d.ts +1 -1
- package/dist/lib/request/helpers/model-map.js +12 -0
- package/dist/lib/request/response-outcome.d.ts +1 -1
- package/dist/lib/request/response-outcome.js +4 -1
- package/dist/lib/runtime/account-status.js +2 -1
- package/dist/lib/runtime/luna-reserve-usage.d.ts +9 -0
- package/dist/lib/runtime/luna-reserve-usage.js +68 -0
- package/dist/lib/runtime/model-discovery-status.d.ts +6 -2
- package/dist/lib/runtime/model-discovery-status.js +13 -5
- package/dist/lib/runtime/reset-credit-routing.js +4 -1
- package/dist/lib/runtime/rotation-server-types.d.ts +2 -0
- package/dist/lib/runtime/runtime-observability.d.ts +2 -0
- package/dist/lib/runtime/runtime-observability.js +1 -0
- package/dist/lib/runtime-rotation-proxy.js +200 -32
- package/dist/lib/usage/pricing.d.ts +1 -1
- package/dist/lib/usage/pricing.js +4 -0
- package/dist/lib/usage/usage-extraction.d.ts +2 -0
- package/dist/lib/usage/usage-extraction.js +25 -1
- package/dist/lib/utils.d.ts +1 -1
- package/dist/lib/utils.js +1 -1
- package/node_modules/@codex-ai/plugin/dist/index.d.ts +2 -2
- package/node_modules/@codex-ai/plugin/dist/index.js +2 -2
- package/node_modules/@codex-ai/plugin/dist/tool.d.ts +42 -42
- package/node_modules/@codex-ai/plugin/dist/tool.js +29 -29
- package/node_modules/@codex-ai/plugin/package.json +9 -9
- package/node_modules/@codex-ai/sdk/dist/index.d.ts +4 -4
- package/node_modules/@codex-ai/sdk/dist/index.js +2 -2
- package/node_modules/@codex-ai/sdk/package.json +8 -8
- package/package.json +207 -207
- package/scripts/codex-app-launcher.js +653 -653
- package/scripts/codex-app-router.js +374 -373
- package/scripts/codex-bin-resolver.js +332 -332
- package/scripts/codex-multi-auth.js +65 -65
- package/scripts/codex-routing.js +62 -62
- package/scripts/codex.js +7006 -7005
- package/scripts/install-codex-auth-utils.js +213 -213
- package/scripts/mcodex.js +367 -367
- package/scripts/postinstall.js +114 -114
- package/scripts/preuninstall.js +265 -265
- package/skills/codex-auth-setup/SKILL.md +73 -73
- package/vendor/codex-ai-plugin/dist/index.d.ts +2 -2
- package/vendor/codex-ai-plugin/dist/index.js +2 -2
- package/vendor/codex-ai-plugin/dist/tool.d.ts +42 -42
- package/vendor/codex-ai-plugin/dist/tool.js +29 -29
- package/vendor/codex-ai-plugin/package.json +9 -9
- package/vendor/codex-ai-sdk/dist/index.d.ts +4 -4
- package/vendor/codex-ai-sdk/dist/index.js +2 -2
- package/vendor/codex-ai-sdk/package.json +8 -8
|
@@ -28,8 +28,9 @@ import { createServer } from "node:http";
|
|
|
28
28
|
import * as zlib from "node:zlib";
|
|
29
29
|
import { AccountManager, AUTH_INVALIDATION_MARKER, extractAccountId, } from "./accounts.js";
|
|
30
30
|
import { withRoutingMutex } from "./routing-mutex.js";
|
|
31
|
+
import { getQuotaKey } from "./accounts/rate-limits.js";
|
|
31
32
|
import { getFetchTimeoutMs, getNetworkErrorCooldownMs, getRetryAllAccountsMaxRetries, getServerErrorCooldownMs, getSessionAffinity, getSessionAffinityMaxEntries, getSessionAffinityTtlMs, getStreamStallTimeoutMs, getMinRotationIntervalMs, getTokenInvalidationCooldownMs, getTokenRefreshSkewMs, getPidOffsetEnabled, getPreemptiveQuotaEnabled, getPreemptiveQuotaMaxDeferralMs, getPreemptiveQuotaRemainingPercent5h, getPreemptiveQuotaRemainingPercent7d, getContextBudgetGuardEnabled, getContextBudgetGuardSoftPercent, getContextBudgetGuardHardPercent, getContextBudgetGuardModelWindowOverrides, getRoutingMutexMode, getSchedulingStrategy, loadPluginConfig, } from "./config.js";
|
|
32
|
-
import { CODEX_BASE_URL, HTTP_STATUS, OPENAI_HEADERS, OPENAI_HEADER_VALUES, URL_PATHS, } from "./constants.js";
|
|
33
|
+
import { CODEX_BASE_URL, HTTP_STATUS, LUNA_RESERVE_MODEL, MAX_RATE_LIMIT_DELAY_MS, OPENAI_HEADERS, OPENAI_HEADER_VALUES, URL_PATHS, } from "./constants.js";
|
|
33
34
|
import { getModelFamily } from "./prompts/codex.js";
|
|
34
35
|
import { CURRENT_CODEX_MODEL } from "./request/helpers/model-map.js";
|
|
35
36
|
import { recordRuntimeInferenceRequest, flushRuntimeInferenceActivity, mutateRuntimeObservabilitySnapshot, recordRuntimeAccountRecovery, recordRuntimePoolExhaustion, } from "./runtime/runtime-observability.js";
|
|
@@ -697,6 +698,38 @@ function buildResponsesRequestContext(req, body) {
|
|
|
697
698
|
stableSessionKey: resolveStableSessionKey(headers, parsedBody),
|
|
698
699
|
};
|
|
699
700
|
}
|
|
701
|
+
const CONFIRMED_QUOTA_EXHAUSTION_PREFIX = "__confirmed-quota-exhaustion:";
|
|
702
|
+
function confirmedQuotaExhaustionKey(family, model) {
|
|
703
|
+
return `${CONFIRMED_QUOTA_EXHAUSTION_PREFIX}${getQuotaKey(family, model)}`;
|
|
704
|
+
}
|
|
705
|
+
/** Rewrite one ordinary Luna request to the separately metered Reserve model. */
|
|
706
|
+
function switchRequestToLunaReserve(context) {
|
|
707
|
+
if (context.model !== "gpt-6-luna" && context.model !== "gpt-5.6-luna")
|
|
708
|
+
return false;
|
|
709
|
+
const body = parseRequestBody(context.body);
|
|
710
|
+
if (!body)
|
|
711
|
+
return false;
|
|
712
|
+
body.model = LUNA_RESERVE_MODEL;
|
|
713
|
+
// Reserve availability is independent from paid Fast service-tier access.
|
|
714
|
+
// Let the backend choose its supported tier instead of carrying a Luna-only
|
|
715
|
+
// preference into the fallback request.
|
|
716
|
+
delete body.service_tier;
|
|
717
|
+
context.body = Buffer.from(JSON.stringify(body));
|
|
718
|
+
context.model = LUNA_RESERVE_MODEL;
|
|
719
|
+
context.family = getModelFamily(LUNA_RESERVE_MODEL);
|
|
720
|
+
return true;
|
|
721
|
+
}
|
|
722
|
+
/** True only for 429s that identify subscription usage/quota exhaustion. */
|
|
723
|
+
function isSubscriptionQuota429(status, bodyText) {
|
|
724
|
+
if (status !== HTTP_STATUS.TOO_MANY_REQUESTS)
|
|
725
|
+
return false;
|
|
726
|
+
const code = extractErrorCodeFromBody(bodyText)?.toLowerCase() ?? "";
|
|
727
|
+
if (code.includes("usage_limit") || code.includes("quota"))
|
|
728
|
+
return true;
|
|
729
|
+
if (code)
|
|
730
|
+
return false;
|
|
731
|
+
return /\busage[ _-]+limit[ _-]+(?:reached|exceeded|exhausted)\b|\bquota[ _-]+(?:reached|exceeded|exhausted)\b/i.test(bodyText);
|
|
732
|
+
}
|
|
700
733
|
function buildImageRequestContext(req, body, pathname) {
|
|
701
734
|
return {
|
|
702
735
|
...buildResponsesRequestContext(req, body),
|
|
@@ -1198,13 +1231,14 @@ async function handleRequestInner(state, req, res, traceId) {
|
|
|
1198
1231
|
}
|
|
1199
1232
|
const requestStartedAt = state.now();
|
|
1200
1233
|
let policyDecision = null;
|
|
1234
|
+
let runtimePolicyState = null;
|
|
1201
1235
|
let projectKey = null;
|
|
1202
1236
|
let policyError = null;
|
|
1203
1237
|
try {
|
|
1204
|
-
|
|
1205
|
-
projectKey =
|
|
1238
|
+
runtimePolicyState = await loadRuntimePolicyState();
|
|
1239
|
+
projectKey = runtimePolicyState.project.projectKey;
|
|
1206
1240
|
policyDecision = await evaluateRuntimePolicy({
|
|
1207
|
-
state:
|
|
1241
|
+
state: runtimePolicyState,
|
|
1208
1242
|
accounts: accountManager.getAccountsSnapshot(),
|
|
1209
1243
|
model: context.model,
|
|
1210
1244
|
now: requestStartedAt,
|
|
@@ -1220,7 +1254,8 @@ async function handleRequestInner(state, req, res, traceId) {
|
|
|
1220
1254
|
policyError = error instanceof Error ? error.message : String(error);
|
|
1221
1255
|
state.status.lastError = policyError;
|
|
1222
1256
|
}
|
|
1223
|
-
|
|
1257
|
+
/** Build a recorder for the model that actually serves this request. */
|
|
1258
|
+
const createUsageRecorderForModel = (model) => createRuntimeUsageRecorder({
|
|
1224
1259
|
source: "runtime-proxy",
|
|
1225
1260
|
operation: isModelsRequest
|
|
1226
1261
|
? "models"
|
|
@@ -1229,11 +1264,12 @@ async function handleRequestInner(state, req, res, traceId) {
|
|
|
1229
1264
|
: isImageRequest
|
|
1230
1265
|
? "images"
|
|
1231
1266
|
: "responses",
|
|
1232
|
-
model
|
|
1267
|
+
model,
|
|
1233
1268
|
projectKey,
|
|
1234
1269
|
requestId: traceId,
|
|
1235
1270
|
startedAt: requestStartedAt,
|
|
1236
1271
|
});
|
|
1272
|
+
usageRecorder = createUsageRecorderForModel(context.model);
|
|
1237
1273
|
if (policyError) {
|
|
1238
1274
|
await usageRecorder.record({
|
|
1239
1275
|
outcome: "failure",
|
|
@@ -1340,11 +1376,11 @@ async function handleRequestInner(state, req, res, traceId) {
|
|
|
1340
1376
|
snapshot.lastAccountUpdatedAt = state.now();
|
|
1341
1377
|
});
|
|
1342
1378
|
});
|
|
1343
|
-
const responseOutcome = new ResponseOutcome(upstream.headers.get("content-type")?.includes("text/event-stream") === true);
|
|
1344
1379
|
const scanner = createUsageStreamScanner({
|
|
1345
1380
|
contentType: upstream.headers.get("content-type"),
|
|
1346
1381
|
onEvent: event => responseOutcome.observe(event),
|
|
1347
1382
|
});
|
|
1383
|
+
const responseOutcome = new ResponseOutcome(() => scanner.isSse());
|
|
1348
1384
|
const forwarded = await forwardStreamingResponse(upstream, res, state.status, () => controller.abort(), state.streamStallTimeoutMs, scanner.push, state.catalogEtag ? { "x-models-etag": state.catalogEtag } : undefined, () => { scanner.result(); return responseOutcome.finish(); });
|
|
1349
1385
|
const attemptedCredential = attemptedCredentialIndex === undefined ? undefined : apiRoutes[attemptedCredentialIndex];
|
|
1350
1386
|
if (!forwarded && attemptedCredential)
|
|
@@ -1483,6 +1519,35 @@ async function handleRequestInner(state, req, res, traceId) {
|
|
|
1483
1519
|
// silently serving from another account.
|
|
1484
1520
|
const pinnedIndex = state.forcedAccountIndex ?? storageMeta.pinnedAccountIndex;
|
|
1485
1521
|
const isPinned = typeof pinnedIndex === "number";
|
|
1522
|
+
let requestModelCatalog;
|
|
1523
|
+
/** Rebuild native routing scopes after Luna is rewritten to separately metered Reserve. */
|
|
1524
|
+
const rebuildReserveWorkspaceCandidates = async () => {
|
|
1525
|
+
const modelCatalog = requestModelCatalog;
|
|
1526
|
+
if (!(state.nativeOpenai && isResponsesRequest && context.model === LUNA_RESERVE_MODEL && modelCatalog))
|
|
1527
|
+
return;
|
|
1528
|
+
const body = parseRequestBody(context.body);
|
|
1529
|
+
const effort = isRecord(body?.reasoning) && typeof body.reasoning.effort === "string" ? body.reasoning.effort : undefined;
|
|
1530
|
+
const tier = typeof body?.service_tier === "string" ? body.service_tier : undefined;
|
|
1531
|
+
if (!state.capabilityFailures)
|
|
1532
|
+
state.capabilityFailures = new RuntimeCapabilityFailures(state.now);
|
|
1533
|
+
const failures = state.capabilityFailures;
|
|
1534
|
+
const eligibleAccounts = accountManager.getAccountsSnapshot().filter((account) => account.enabled !== false && (!isPinned || account.index === pinnedIndex) && !policyDecision?.blockedAccountIndexes.has(account.index));
|
|
1535
|
+
const routableScopes = eligibleAccounts.flatMap(workspaceModelScopes).filter((scope) => scope.routable);
|
|
1536
|
+
await modelCatalog.prepareRouting(routableScopes.map((scope) => scope.id), context.model, effort, tier, (key) => failures.supports(key, context.model ?? LUNA_RESERVE_MODEL, effort, tier));
|
|
1537
|
+
workspaceCandidates.clear();
|
|
1538
|
+
catalogEligibleKeys = new Set();
|
|
1539
|
+
for (const key of Object.keys(subscriptionQuotaByAccount))
|
|
1540
|
+
delete subscriptionQuotaByAccount[Number(key)];
|
|
1541
|
+
for (const account of eligibleAccounts) {
|
|
1542
|
+
const candidates = workspaceModelScopes(account).filter((scope) => scope.routable)
|
|
1543
|
+
.sort((a, b) => Number(b.selected) - Number(a.selected) || Number(b.bound) - Number(a.bound))
|
|
1544
|
+
.filter((scope) => failures.supports(scope.id, context.model ?? LUNA_RESERVE_MODEL, effort, tier) && modelCatalog.supportsForRouting(scope.id, context.model ?? LUNA_RESERVE_MODEL, effort, tier));
|
|
1545
|
+
if (!candidates.length)
|
|
1546
|
+
continue;
|
|
1547
|
+
workspaceCandidates.set(account.index, candidates);
|
|
1548
|
+
catalogEligibleKeys.add(catalogAccountKey(account));
|
|
1549
|
+
}
|
|
1550
|
+
};
|
|
1486
1551
|
if (state.nativeOpenai && (isModelsRequest || (isResponsesRequest && context.model))) {
|
|
1487
1552
|
const requestedVersion = incomingUrl.searchParams.get("client_version") ?? incomingHeaders.get("version");
|
|
1488
1553
|
const catalogClientVersion = requestedVersion && /^[0-9A-Za-z.+_-]{1,80}$/.test(requestedVersion) ? requestedVersion : undefined;
|
|
@@ -1557,6 +1622,7 @@ async function handleRequestInner(state, req, res, traceId) {
|
|
|
1557
1622
|
return JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
1558
1623
|
}, state.now);
|
|
1559
1624
|
const modelCatalog = state.modelCatalog;
|
|
1625
|
+
requestModelCatalog = modelCatalog;
|
|
1560
1626
|
if (catalogsByVersion.size >= 4 && !catalogsByVersion.has(versionKey))
|
|
1561
1627
|
catalogsByVersion.delete(catalogsByVersion.keys().next().value ?? "");
|
|
1562
1628
|
catalogsByVersion.set(versionKey, modelCatalog);
|
|
@@ -1700,9 +1766,11 @@ async function handleRequestInner(state, req, res, traceId) {
|
|
|
1700
1766
|
candidates.push(scope);
|
|
1701
1767
|
}
|
|
1702
1768
|
if (candidates.length) {
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1769
|
+
if (requestedModel !== LUNA_RESERVE_MODEL) {
|
|
1770
|
+
candidates.sort((a, b) => compareSubscriptionQuota(subscriptionQuotaPreference(quotaForScope(account, a), state.now()), subscriptionQuotaPreference(quotaForScope(account, b), state.now())));
|
|
1771
|
+
const first = candidates[0];
|
|
1772
|
+
subscriptionQuotaByAccount[account.index] = subscriptionQuotaPreference(first ? quotaForScope(account, first) : null, state.now());
|
|
1773
|
+
}
|
|
1706
1774
|
workspaceCandidates.set(account.index, candidates);
|
|
1707
1775
|
catalogEligibleKeys.add(catalogAccountKey(account));
|
|
1708
1776
|
supported++;
|
|
@@ -1713,7 +1781,7 @@ async function handleRequestInner(state, req, res, traceId) {
|
|
|
1713
1781
|
return;
|
|
1714
1782
|
}
|
|
1715
1783
|
}
|
|
1716
|
-
if (state.nativeOpenai && isResponsesRequest && context.model && resetCreditState?.lastRedemptionAt !== undefined) {
|
|
1784
|
+
if (state.nativeOpenai && isResponsesRequest && context.model && context.model !== LUNA_RESERVE_MODEL && resetCreditState?.lastRedemptionAt !== undefined) {
|
|
1717
1785
|
for (const [index, scopes] of workspaceCandidates) {
|
|
1718
1786
|
const account = accountManager.getAccountByIndex(index);
|
|
1719
1787
|
if (!account)
|
|
@@ -1724,7 +1792,7 @@ async function handleRequestInner(state, req, res, traceId) {
|
|
|
1724
1792
|
observations: state.subscriptionQuotaObservations ??= new Map(), now: state.now(), clearQuotaScheduler: a => state.preemptiveQuotaScheduler.clear(buildQuotaScheduleKey(a, context.family, context.model)) });
|
|
1725
1793
|
}
|
|
1726
1794
|
}
|
|
1727
|
-
if (state.nativeOpenai && isResponsesRequest && context.model && !isPinned) {
|
|
1795
|
+
if (state.nativeOpenai && isResponsesRequest && context.model && context.model !== LUNA_RESERVE_MODEL && !isPinned) {
|
|
1728
1796
|
try {
|
|
1729
1797
|
await recoverResetQuota({ model: context.model, family: context.family, native: true, pinned: false, manager: accountManager,
|
|
1730
1798
|
scopes: workspaceCandidates, quotaForScope, priorityByAccount: policyDecision?.priorityByAccount, preferredIndex: storageMeta.pinnedAccountIndex, service: createResetCreditService(accountManager),
|
|
@@ -1738,6 +1806,52 @@ async function handleRequestInner(state, req, res, traceId) {
|
|
|
1738
1806
|
// Recovery may have redeemed and written new reset state; the next request re-reads it.
|
|
1739
1807
|
state.readCache?.delete("reset-credits");
|
|
1740
1808
|
}
|
|
1809
|
+
// A pinned Luna request can arrive after an earlier turn already proved
|
|
1810
|
+
// ordinary subscription quota exhausted. That persisted family marker
|
|
1811
|
+
// would make chooseAccount reject the hard pin before any upstream call,
|
|
1812
|
+
// so move the request to the independently metered Reserve model first.
|
|
1813
|
+
if (isPinned &&
|
|
1814
|
+
(context.model === "gpt-6-luna" || context.model === "gpt-5.6-luna")) {
|
|
1815
|
+
const pinnedAccount = accountManager.getAccountByIndex(pinnedIndex);
|
|
1816
|
+
const now = state.now();
|
|
1817
|
+
const familyResetAt = pinnedAccount?.rateLimitResetTimes[getQuotaKey(context.family)];
|
|
1818
|
+
const modelResetAt = pinnedAccount?.rateLimitResetTimes[getQuotaKey(context.family, context.model)];
|
|
1819
|
+
const confirmedQuotaResetAt = pinnedAccount?.rateLimitResetTimes[confirmedQuotaExhaustionKey(context.family, context.model)];
|
|
1820
|
+
const persistedQuotaExhausted = typeof familyResetAt === "number" &&
|
|
1821
|
+
familyResetAt > now &&
|
|
1822
|
+
typeof confirmedQuotaResetAt === "number" &&
|
|
1823
|
+
confirmedQuotaResetAt > now &&
|
|
1824
|
+
!(typeof modelResetAt === "number" && modelResetAt > now);
|
|
1825
|
+
if (persistedQuotaExhausted && switchRequestToLunaReserve(context)) {
|
|
1826
|
+
usageRecorder = createUsageRecorderForModel(context.model);
|
|
1827
|
+
if (runtimePolicyState) {
|
|
1828
|
+
try {
|
|
1829
|
+
const reservePolicy = await evaluateRuntimePolicy({ state: runtimePolicyState, accounts: accountManager.getAccountsSnapshot(), model: context.model, now });
|
|
1830
|
+
policyDecision = reservePolicy;
|
|
1831
|
+
mutateRuntimeObservabilitySnapshot((snapshot) => {
|
|
1832
|
+
snapshot.policyBlockedIndexes = [...reservePolicy.blockedAccountIndexes];
|
|
1833
|
+
snapshot.policyBlockedReasons = Object.fromEntries([...reservePolicy.blockedAccountIndexes].map((index) => [String(index), "policy-blocked"]));
|
|
1834
|
+
});
|
|
1835
|
+
if (!reservePolicy.allowed) {
|
|
1836
|
+
await usageRecorder.record({ outcome: "blocked", statusCode: reservePolicy.statusCode, errorCode: reservePolicy.errorCode });
|
|
1837
|
+
writeJson(res, reservePolicy.statusCode, { error: { message: "Runtime policy blocked Luna Reserve fallback.", code: reservePolicy.errorCode ?? "policy_blocked", reasons: reservePolicy.reasons } });
|
|
1838
|
+
return;
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1841
|
+
catch {
|
|
1842
|
+
await usageRecorder.record({ outcome: "failure", statusCode: HTTP_STATUS.SERVICE_UNAVAILABLE, errorCode: "runtime_policy_unavailable" });
|
|
1843
|
+
writeJson(res, HTTP_STATUS.SERVICE_UNAVAILABLE, { error: { message: "Runtime policy could not be loaded for Luna Reserve fallback.", code: "runtime_policy_unavailable" } });
|
|
1844
|
+
return;
|
|
1845
|
+
}
|
|
1846
|
+
}
|
|
1847
|
+
attemptedIndexes.clear();
|
|
1848
|
+
accountSkipReasons.clear();
|
|
1849
|
+
await rebuildReserveWorkspaceCandidates();
|
|
1850
|
+
state.status.lunaReserveFallbacks = (state.status.lunaReserveFallbacks ?? 0) + 1;
|
|
1851
|
+
mutateRuntimeObservabilitySnapshot((snapshot) => { snapshot.lunaReserveFallbacks = (snapshot.lunaReserveFallbacks ?? 0) + 1; });
|
|
1852
|
+
proxyLog.info("persisted Luna quota exhausted; routing pinned request through Luna Reserve", { traceId });
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1741
1855
|
// The token bucket spreads load across a selectable pool. A pin has no
|
|
1742
1856
|
// alternative account, so exhausting that local heuristic can only reject a
|
|
1743
1857
|
// request that the pinned account could serve. Keep circuit-breaker admission
|
|
@@ -1868,14 +1982,16 @@ async function handleRequestInner(state, req, res, traceId) {
|
|
|
1868
1982
|
// calls inside `chooseAccount` are used unchanged and no lock is taken,
|
|
1869
1983
|
// so default behavior and perf are identical to before.
|
|
1870
1984
|
const selectAccount = () => {
|
|
1871
|
-
|
|
1872
|
-
const
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1985
|
+
if (context.model !== LUNA_RESERVE_MODEL) {
|
|
1986
|
+
for (const [index, scopes] of workspaceCandidates) {
|
|
1987
|
+
const account = accountManager.getAccountByIndex(index);
|
|
1988
|
+
if (!account)
|
|
1989
|
+
continue;
|
|
1990
|
+
scopes.sort((a, b) => compareSubscriptionQuota(subscriptionQuotaPreference(quotaForScope(account, a), state.now()), subscriptionQuotaPreference(quotaForScope(account, b), state.now())));
|
|
1991
|
+
const scope = scopes[0];
|
|
1992
|
+
if (scope)
|
|
1993
|
+
subscriptionQuotaByAccount[index] = subscriptionQuotaPreference(quotaForScope(account, scope), state.now());
|
|
1994
|
+
}
|
|
1879
1995
|
}
|
|
1880
1996
|
const result = chooseAccount({
|
|
1881
1997
|
accountManager,
|
|
@@ -1888,7 +2004,7 @@ async function handleRequestInner(state, req, res, traceId) {
|
|
|
1888
2004
|
policy: policyDecision,
|
|
1889
2005
|
pinnedIndex,
|
|
1890
2006
|
preferredIndex,
|
|
1891
|
-
subscriptionQuotaByAccount: state.nativeOpenai && isResponsesRequest ? subscriptionQuotaByAccount : undefined,
|
|
2007
|
+
subscriptionQuotaByAccount: state.nativeOpenai && isResponsesRequest && context.model !== LUNA_RESERVE_MODEL ? subscriptionQuotaByAccount : undefined,
|
|
1892
2008
|
fallbackPinnedIndex: state.nativeOpenai ? storageMeta.pinnedAccountIndex : null,
|
|
1893
2009
|
skipReasons: accountSkipReasons,
|
|
1894
2010
|
stickyBoostByAccount: rotationStickyBoost,
|
|
@@ -1964,7 +2080,9 @@ async function handleRequestInner(state, req, res, traceId) {
|
|
|
1964
2080
|
attemptedIndexes.add(selected.index);
|
|
1965
2081
|
const quotaScheduleKey = buildQuotaScheduleKey(selected, context.family, context.model);
|
|
1966
2082
|
const preemptiveDeferral = state.preemptiveQuotaScheduler.getDeferral(quotaScheduleKey, state.now());
|
|
1967
|
-
if (
|
|
2083
|
+
if (context.model !== LUNA_RESERVE_MODEL &&
|
|
2084
|
+
preemptiveDeferral.defer &&
|
|
2085
|
+
preemptiveDeferral.waitMs > 0) {
|
|
1968
2086
|
accountSkipReasons.set(selected.index, preemptiveDeferral.reason ?? "quota-near-exhaustion");
|
|
1969
2087
|
exhaustionReason = "rate-limit";
|
|
1970
2088
|
capabilityRejectionIsLatest = false;
|
|
@@ -2253,8 +2371,9 @@ async function handleRequestInner(state, req, res, traceId) {
|
|
|
2253
2371
|
}
|
|
2254
2372
|
};
|
|
2255
2373
|
const quotaSnapshot = readQuotaSchedulerSnapshot(upstream.headers, upstream.status, state.now());
|
|
2256
|
-
if (quotaSnapshot)
|
|
2374
|
+
if (quotaSnapshot && context.model !== LUNA_RESERVE_MODEL) {
|
|
2257
2375
|
observeQuota(quotaSnapshot, upstream.headers.get("x-codex-plan-type") ?? undefined);
|
|
2376
|
+
}
|
|
2258
2377
|
if (isResponsesRequest && context.model && [400, 403, 404].includes(upstream.status)) {
|
|
2259
2378
|
const errorBody = await readErrorBody(upstream, state.streamStallTimeoutMs, 65536);
|
|
2260
2379
|
let data;
|
|
@@ -2309,7 +2428,9 @@ async function handleRequestInner(state, req, res, traceId) {
|
|
|
2309
2428
|
// A capacity 429 is not this account's quota. Marking it rate
|
|
2310
2429
|
// limited would take a healthy account out of the pool for the
|
|
2311
2430
|
// retry-after window on an outage that affects every account.
|
|
2312
|
-
|
|
2431
|
+
const modelAtCapacity = isModelAtCapacityError(upstream.status, bodyText);
|
|
2432
|
+
const subscriptionQuotaExceeded = isSubscriptionQuota429(upstream.status, bodyText);
|
|
2433
|
+
if (modelAtCapacity) {
|
|
2313
2434
|
const outcome = await waitOutModelCapacity(retryAfterHintMs, refreshed.account);
|
|
2314
2435
|
if (outcome === "client-gone") {
|
|
2315
2436
|
await usageRecorder.record({
|
|
@@ -2323,12 +2444,55 @@ async function handleRequestInner(state, req, res, traceId) {
|
|
|
2323
2444
|
if (outcome === "retry")
|
|
2324
2445
|
continue;
|
|
2325
2446
|
}
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2447
|
+
if (context.model !== LUNA_RESERVE_MODEL && subscriptionQuotaExceeded) {
|
|
2448
|
+
state.preemptiveQuotaScheduler.markRateLimited(quotaScheduleKey, retryAfterMs, state.now());
|
|
2449
|
+
}
|
|
2450
|
+
// Every 429 still drains this account for its retry window, but only an
|
|
2451
|
+
// explicit subscription-usage signal is persisted as quota exhaustion.
|
|
2329
2452
|
accountManager.recordRateLimit(refreshed.account, context.family, context.model);
|
|
2330
|
-
accountManager.markRateLimitedWithReason(refreshed.account, retryAfterMs, context.family, "quota", context.model);
|
|
2453
|
+
accountManager.markRateLimitedWithReason(refreshed.account, retryAfterMs, context.family, subscriptionQuotaExceeded ? "quota" : "unknown", context.model);
|
|
2454
|
+
if (subscriptionQuotaExceeded &&
|
|
2455
|
+
(context.model === "gpt-6-luna" || context.model === "gpt-5.6-luna")) {
|
|
2456
|
+
const confirmedKey = confirmedQuotaExhaustionKey(context.family, context.model);
|
|
2457
|
+
const confirmedResetAt = state.now() +
|
|
2458
|
+
Math.min(Math.max(0, Math.floor(retryAfterMs)), MAX_RATE_LIMIT_DELAY_MS);
|
|
2459
|
+
refreshed.account.rateLimitResetTimes[confirmedKey] = Math.max(refreshed.account.rateLimitResetTimes[confirmedKey] ?? 0, confirmedResetAt);
|
|
2460
|
+
}
|
|
2331
2461
|
accountManager.saveToDiskDebounced();
|
|
2462
|
+
if (!modelAtCapacity && subscriptionQuotaExceeded && switchRequestToLunaReserve(context)) {
|
|
2463
|
+
usageRecorder = createUsageRecorderForModel(context.model);
|
|
2464
|
+
if (runtimePolicyState) {
|
|
2465
|
+
try {
|
|
2466
|
+
const reservePolicy = await evaluateRuntimePolicy({ state: runtimePolicyState, accounts: accountManager.getAccountsSnapshot(), model: context.model, now: state.now() });
|
|
2467
|
+
policyDecision = reservePolicy;
|
|
2468
|
+
mutateRuntimeObservabilitySnapshot((snapshot) => { snapshot.policyBlockedIndexes = [...reservePolicy.blockedAccountIndexes]; snapshot.policyBlockedReasons = Object.fromEntries([...reservePolicy.blockedAccountIndexes].map((index) => [String(index), "policy-blocked"])); });
|
|
2469
|
+
if (!reservePolicy.allowed) {
|
|
2470
|
+
await usageRecorder.record({ outcome: "blocked", statusCode: reservePolicy.statusCode, errorCode: reservePolicy.errorCode });
|
|
2471
|
+
writeJson(res, reservePolicy.statusCode, { error: { message: "Runtime policy blocked Luna Reserve fallback.", code: reservePolicy.errorCode ?? "policy_blocked", reasons: reservePolicy.reasons } });
|
|
2472
|
+
return;
|
|
2473
|
+
}
|
|
2474
|
+
}
|
|
2475
|
+
catch {
|
|
2476
|
+
await usageRecorder.record({ outcome: "failure", statusCode: HTTP_STATUS.SERVICE_UNAVAILABLE, errorCode: "runtime_policy_unavailable" });
|
|
2477
|
+
writeJson(res, HTTP_STATUS.SERVICE_UNAVAILABLE, { error: { message: "Runtime policy could not be loaded for Luna Reserve fallback.", code: "runtime_policy_unavailable" } });
|
|
2478
|
+
return;
|
|
2479
|
+
}
|
|
2480
|
+
}
|
|
2481
|
+
attemptedIndexes.clear();
|
|
2482
|
+
accountSkipReasons.clear();
|
|
2483
|
+
rejectedWorkspaces.clear();
|
|
2484
|
+
await rebuildReserveWorkspaceCandidates();
|
|
2485
|
+
state.status.lunaReserveFallbacks = (state.status.lunaReserveFallbacks ?? 0) + 1;
|
|
2486
|
+
mutateRuntimeObservabilitySnapshot((snapshot) => { snapshot.lunaReserveFallbacks = (snapshot.lunaReserveFallbacks ?? 0) + 1; });
|
|
2487
|
+
proxyLog.info("ordinary Luna quota exhausted; retrying with Luna Reserve", {
|
|
2488
|
+
traceId,
|
|
2489
|
+
pinned: isPinned,
|
|
2490
|
+
});
|
|
2491
|
+
capabilityRejectionIsLatest = false;
|
|
2492
|
+
exhaustionReason = "no-account";
|
|
2493
|
+
state.status.retries += 1;
|
|
2494
|
+
continue;
|
|
2495
|
+
}
|
|
2332
2496
|
exhaustionReason = "rate-limit";
|
|
2333
2497
|
capabilityRejectionIsLatest = false;
|
|
2334
2498
|
transientAttempts += 1;
|
|
@@ -2544,9 +2708,11 @@ async function handleRequestInner(state, req, res, traceId) {
|
|
|
2544
2708
|
// the forecast keeps reporting this working account as unavailable.
|
|
2545
2709
|
// No-op when no reason is recorded, so the hot path stays write-free.
|
|
2546
2710
|
const quotaDeferral = state.preemptiveQuotaScheduler.getDeferral(quotaScheduleKey, state.now());
|
|
2547
|
-
const nearExhaustionWaitMs =
|
|
2548
|
-
?
|
|
2549
|
-
:
|
|
2711
|
+
const nearExhaustionWaitMs = context.model === LUNA_RESERVE_MODEL
|
|
2712
|
+
? 0
|
|
2713
|
+
: quotaDeferral.defer
|
|
2714
|
+
? quotaDeferral.waitMs
|
|
2715
|
+
: 0;
|
|
2550
2716
|
if (nearExhaustionWaitMs > 0) {
|
|
2551
2717
|
accountManager.markRateLimitedWithReason(refreshed.account, nearExhaustionWaitMs, context.family, "quota", context.model);
|
|
2552
2718
|
state.sessionAffinityStore?.forgetSession(context.sessionKey);
|
|
@@ -2565,11 +2731,12 @@ async function handleRequestInner(state, req, res, traceId) {
|
|
|
2565
2731
|
// evaluateBudgetGuard compares `0 >= limit` for maxTokens/maxCostUsd
|
|
2566
2732
|
// and those caps never fire — `budget set --cost 50` would allow
|
|
2567
2733
|
// unlimited spend, with only --requests actually enforced.
|
|
2568
|
-
const responseOutcome = new ResponseOutcome(isResponsesRequest && upstream.headers.get("content-type")?.includes("text/event-stream") === true);
|
|
2569
2734
|
const usageScanner = createUsageStreamScanner({
|
|
2570
2735
|
contentType: upstream.headers.get("content-type"),
|
|
2571
2736
|
onEvent: (event) => {
|
|
2572
2737
|
responseOutcome.observe(event);
|
|
2738
|
+
if (context.model === LUNA_RESERVE_MODEL)
|
|
2739
|
+
return;
|
|
2573
2740
|
const snapshot = readSubscriptionQuotaEvent(event, state.now());
|
|
2574
2741
|
if (!snapshot)
|
|
2575
2742
|
return;
|
|
@@ -2578,6 +2745,7 @@ async function handleRequestInner(state, req, res, traceId) {
|
|
|
2578
2745
|
state.status.lastStreamQuotaUpdateAt = snapshot.updatedAt;
|
|
2579
2746
|
},
|
|
2580
2747
|
});
|
|
2748
|
+
const responseOutcome = new ResponseOutcome(() => isResponsesRequest && usageScanner.isSse());
|
|
2581
2749
|
let streamErrored = false;
|
|
2582
2750
|
const forwarded = await forwardStreamingResponse(upstream, res, state.status, () => {
|
|
2583
2751
|
streamErrored = true;
|
|
@@ -51,7 +51,7 @@ export declare const SHORT_CONTEXT_MAX_INPUT_TOKENS = 272000;
|
|
|
51
51
|
* `test/usage-pricing-coverage.test.ts` fails if a NEW routable model appears
|
|
52
52
|
* in neither list.
|
|
53
53
|
*/
|
|
54
|
-
export declare const UNPRICED_ROUTABLE_MODELS: readonly ["gpt-daybreak-blue-latest", "gpt-daybreak-red-latest"];
|
|
54
|
+
export declare const UNPRICED_ROUTABLE_MODELS: readonly ["gpt-reserve", "gpt-daybreak-blue-latest", "gpt-daybreak-red-latest"];
|
|
55
55
|
export declare function getUsageModelPricing(model: string | null | undefined): UsageModelPricing | null;
|
|
56
56
|
export declare function estimateUsageCostUsd(model: string | null | undefined, tokens: UsageTokenCounts): number | null;
|
|
57
57
|
export declare function listUsageModelPricing(): Record<string, UsageModelPricing>;
|
|
@@ -168,6 +168,10 @@ const MODEL_PRICING = {
|
|
|
168
168
|
* in neither list.
|
|
169
169
|
*/
|
|
170
170
|
export const UNPRICED_ROUTABLE_MODELS = [
|
|
171
|
+
// Luna Reserve is a separately metered subscription allowance. The backend
|
|
172
|
+
// exposes usage percentage, not a public per-token dollar rate; cost budgets
|
|
173
|
+
// therefore remain fail-closed instead of borrowing Luna API pricing.
|
|
174
|
+
"gpt-reserve",
|
|
171
175
|
// The Daybreak cyber models are sold under a separate controlled-access
|
|
172
176
|
// agreement with no public per-token rate.
|
|
173
177
|
"gpt-daybreak-blue-latest",
|
|
@@ -22,6 +22,8 @@ export declare function extractUsageTokenCounts(usage: unknown): UsageTokenCount
|
|
|
22
22
|
*/
|
|
23
23
|
export declare function extractResponsesUsage(payload: unknown): UsageTokenCounts | null;
|
|
24
24
|
export interface UsageStreamScanner {
|
|
25
|
+
/** The shared format decision, updated as the bounded body prefix arrives. */
|
|
26
|
+
isSse: () => boolean;
|
|
25
27
|
/** Feed one forwarded chunk. Never throws. */
|
|
26
28
|
push: (chunk: Uint8Array) => void;
|
|
27
29
|
/** Flush any trailing partial data and return the final counts, if any. */
|
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
*/
|
|
10
10
|
/** Cap on retained bytes so a hostile or unterminated body cannot grow forever. */
|
|
11
11
|
const MAX_BUFFERED_BYTES = 1_048_576;
|
|
12
|
+
/** Only inspect this many leading bytes when the media type does not identify SSE. */
|
|
13
|
+
const MAX_SNIFF_BYTES = 4_096;
|
|
12
14
|
/** Terminal Responses stream events that carry the final `usage` object. */
|
|
13
15
|
const TERMINAL_EVENT_TYPES = new Set([
|
|
14
16
|
"response.completed",
|
|
@@ -132,7 +134,11 @@ export function extractResponsesUsage(payload) {
|
|
|
132
134
|
* bound.
|
|
133
135
|
*/
|
|
134
136
|
export function createUsageStreamScanner(options) {
|
|
135
|
-
|
|
137
|
+
let sse = (options.contentType ?? "").toLowerCase().includes("text/event-stream");
|
|
138
|
+
let sniffing = !sse;
|
|
139
|
+
let sniffedBytes = 0;
|
|
140
|
+
let sniffPrefix = "";
|
|
141
|
+
const sniffDecoder = new TextDecoder("utf-8");
|
|
136
142
|
const decoder = new TextDecoder("utf-8");
|
|
137
143
|
let pending = "";
|
|
138
144
|
let bufferedBytes = 0;
|
|
@@ -190,11 +196,29 @@ export function createUsageStreamScanner(options) {
|
|
|
190
196
|
}
|
|
191
197
|
eventData.push(value);
|
|
192
198
|
};
|
|
199
|
+
const sniffFormat = (chunk) => {
|
|
200
|
+
if (!sniffing)
|
|
201
|
+
return;
|
|
202
|
+
const prefix = chunk.subarray(0, MAX_SNIFF_BYTES - sniffedBytes);
|
|
203
|
+
sniffedBytes += prefix.byteLength;
|
|
204
|
+
sniffPrefix += sniffDecoder.decode(prefix, { stream: true });
|
|
205
|
+
// Match only at the start (after blank lines/BOM), never a data: string
|
|
206
|
+
// inside JSON. A field or comment identifies SSE before a possibly huge
|
|
207
|
+
// event arrives, so streaming never depends on buffering its whole frame.
|
|
208
|
+
sse = /^(?:\r?\n)*(?::|(?:data|event|id|retry):)/.test(sniffPrefix);
|
|
209
|
+
const first = sniffPrefix.trimStart()[0];
|
|
210
|
+
if (sse || first === "{" || first === "[" || sniffedBytes >= MAX_SNIFF_BYTES) {
|
|
211
|
+
sniffing = false;
|
|
212
|
+
sniffPrefix = "";
|
|
213
|
+
}
|
|
214
|
+
};
|
|
193
215
|
return {
|
|
216
|
+
isSse: () => sse,
|
|
194
217
|
push: (chunk) => {
|
|
195
218
|
try {
|
|
196
219
|
if (overflowed || chunk.byteLength === 0)
|
|
197
220
|
return;
|
|
221
|
+
sniffFormat(chunk);
|
|
198
222
|
bufferedBytes += chunk.byteLength;
|
|
199
223
|
pending += decoder.decode(chunk, { stream: true });
|
|
200
224
|
if (!sse) {
|
package/dist/lib/utils.d.ts
CHANGED
|
@@ -18,7 +18,7 @@ export declare function isAbortError(error: unknown): boolean;
|
|
|
18
18
|
* Combines two abort signals into one that aborts when the FIRST input does.
|
|
19
19
|
*
|
|
20
20
|
* This is a thin null-tolerant wrapper over `AbortSignal.any` — available
|
|
21
|
-
*
|
|
21
|
+
* on every Node version the engines floor (`>=22.19`) permits. Using the
|
|
22
22
|
* native composite matters beyond brevity: a manual addEventListener pair
|
|
23
23
|
* stays registered on BOTH inputs until one fires, so a caller that combines
|
|
24
24
|
* a long-lived signal (e.g. a proxy-lifetime abort) once per request would
|
package/dist/lib/utils.js
CHANGED
|
@@ -25,7 +25,7 @@ export function isAbortError(error) {
|
|
|
25
25
|
* Combines two abort signals into one that aborts when the FIRST input does.
|
|
26
26
|
*
|
|
27
27
|
* This is a thin null-tolerant wrapper over `AbortSignal.any` — available
|
|
28
|
-
*
|
|
28
|
+
* on every Node version the engines floor (`>=22.19`) permits. Using the
|
|
29
29
|
* native composite matters beyond brevity: a manual addEventListener pair
|
|
30
30
|
* stays registered on BOTH inputs until one fires, so a caller that combines
|
|
31
31
|
* a long-lived signal (e.g. a proxy-lifetime abort) once per request would
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export type PluginInput = any;
|
|
2
|
-
export type Plugin = any;
|
|
1
|
+
export type PluginInput = any;
|
|
2
|
+
export type Plugin = any;
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export {};
|
|
2
|
-
|
|
1
|
+
export {};
|
|
2
|
+
|
|
@@ -1,42 +1,42 @@
|
|
|
1
|
-
export interface ToolContext {
|
|
2
|
-
[key: string]: any;
|
|
3
|
-
directory: string;
|
|
4
|
-
worktree: string;
|
|
5
|
-
ask: (request: any) => Promise<any> | any;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export interface ToolDefinition<TArgs = any, TResult = any> {
|
|
9
|
-
description?: string;
|
|
10
|
-
args?: Record<string, any>;
|
|
11
|
-
parameters?: unknown;
|
|
12
|
-
execute: (args: TArgs, context: ToolContext) => Promise<TResult> | TResult;
|
|
13
|
-
[key: string]: any;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export interface SchemaNode {
|
|
17
|
-
optional: () => SchemaNode;
|
|
18
|
-
describe: (_text: string) => SchemaNode;
|
|
19
|
-
min: (_value: number) => SchemaNode;
|
|
20
|
-
max: (_value: number) => SchemaNode;
|
|
21
|
-
default: (_value: unknown) => SchemaNode;
|
|
22
|
-
int: () => SchemaNode;
|
|
23
|
-
array: (_node?: unknown) => SchemaNode;
|
|
24
|
-
[key: string]: any;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export interface ToolFactory {
|
|
28
|
-
<TArgs = any, TResult = any>(definition: ToolDefinition<TArgs, TResult>): ToolDefinition<TArgs, TResult>;
|
|
29
|
-
schema: {
|
|
30
|
-
string: () => SchemaNode;
|
|
31
|
-
number: () => SchemaNode;
|
|
32
|
-
boolean: () => SchemaNode;
|
|
33
|
-
object: (shape: Record<string, unknown>) => SchemaNode;
|
|
34
|
-
enum: (values: readonly unknown[]) => SchemaNode;
|
|
35
|
-
literal: (value: unknown) => SchemaNode;
|
|
36
|
-
array: (node: unknown) => SchemaNode;
|
|
37
|
-
any: () => SchemaNode;
|
|
38
|
-
unknown: () => SchemaNode;
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export declare const tool: ToolFactory;
|
|
1
|
+
export interface ToolContext {
|
|
2
|
+
[key: string]: any;
|
|
3
|
+
directory: string;
|
|
4
|
+
worktree: string;
|
|
5
|
+
ask: (request: any) => Promise<any> | any;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface ToolDefinition<TArgs = any, TResult = any> {
|
|
9
|
+
description?: string;
|
|
10
|
+
args?: Record<string, any>;
|
|
11
|
+
parameters?: unknown;
|
|
12
|
+
execute: (args: TArgs, context: ToolContext) => Promise<TResult> | TResult;
|
|
13
|
+
[key: string]: any;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface SchemaNode {
|
|
17
|
+
optional: () => SchemaNode;
|
|
18
|
+
describe: (_text: string) => SchemaNode;
|
|
19
|
+
min: (_value: number) => SchemaNode;
|
|
20
|
+
max: (_value: number) => SchemaNode;
|
|
21
|
+
default: (_value: unknown) => SchemaNode;
|
|
22
|
+
int: () => SchemaNode;
|
|
23
|
+
array: (_node?: unknown) => SchemaNode;
|
|
24
|
+
[key: string]: any;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface ToolFactory {
|
|
28
|
+
<TArgs = any, TResult = any>(definition: ToolDefinition<TArgs, TResult>): ToolDefinition<TArgs, TResult>;
|
|
29
|
+
schema: {
|
|
30
|
+
string: () => SchemaNode;
|
|
31
|
+
number: () => SchemaNode;
|
|
32
|
+
boolean: () => SchemaNode;
|
|
33
|
+
object: (shape: Record<string, unknown>) => SchemaNode;
|
|
34
|
+
enum: (values: readonly unknown[]) => SchemaNode;
|
|
35
|
+
literal: (value: unknown) => SchemaNode;
|
|
36
|
+
array: (node: unknown) => SchemaNode;
|
|
37
|
+
any: () => SchemaNode;
|
|
38
|
+
unknown: () => SchemaNode;
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export declare const tool: ToolFactory;
|