@bitkyc08/opencodex 2.45.0 → 2.46.0-preview.20260907
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-J96sug5C.css → index-BFgUC17B.css} +1 -1
- package/gui/dist/assets/{index-CCfD72yq.js → index-NcAVXkST.js} +19 -19
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/raycast.svg +3 -0
- package/package.json +1 -1
- package/src/adapters/anthropic.ts +8 -3
- package/src/adapters/openai-responses.ts +7 -0
- package/src/bridge.ts +27 -7
- package/src/claude/inbound.ts +23 -5
- package/src/claude/outbound.ts +87 -19
- package/src/cli/capabilities.ts +4 -1
- package/src/cli/dispatch.ts +1 -1
- package/src/cli/doctor.ts +2 -2
- package/src/cli/export-command.ts +22 -10
- package/src/cli/help.ts +1 -1
- package/src/cli/index.ts +34 -7
- package/src/cli/integrations.ts +34 -1
- package/src/cli/provider.ts +27 -15
- package/src/cli/registry.ts +2 -2
- package/src/cli/version-skew.ts +36 -3
- package/src/clients/aside-profiles.ts +8 -7
- package/src/clients/config-export/contracts.ts +2 -1
- package/src/clients/config-export/raycast.ts +106 -0
- package/src/clients/config-export.ts +36 -0
- package/src/clients/model-presentation.ts +61 -0
- package/src/codex/catalog/sync.ts +44 -2
- package/src/codex/convergence.ts +7 -0
- package/src/generated/compatibility-version.json +59 -43
- package/src/generated/model-metadata.ts +1 -0
- package/src/images/loop.ts +10 -2
- package/src/integrations/catalog-refresh.ts +1 -1
- package/src/integrations/merge.ts +158 -25
- package/src/integrations/raycast-detect.ts +111 -0
- package/src/integrations/registry.ts +18 -0
- package/src/integrations/state.ts +82 -13
- package/src/integrations/writer.ts +46 -31
- package/src/lib/bounded-body.ts +22 -7
- package/src/oauth/anthropic-routing.ts +59 -14
- package/src/oauth/health.ts +3 -0
- package/src/providers/quota.ts +145 -9
- package/src/providers/registry.ts +31 -0
- package/src/responses/parser.ts +16 -3
- package/src/responses/reasoning-envelope.ts +3 -2
- package/src/server/grok-responses-control-frame.ts +43 -0
- package/src/server/management/config-routes.ts +6 -2
- package/src/server/management/integration-routes.ts +29 -2
- package/src/server/management/model-routes.ts +9 -1
- package/src/server/request-decompress.ts +34 -8
- package/src/server/responses/agent-task-recovery-cache.ts +43 -14
- package/src/server/responses/agent-task-recovery.ts +28 -16
- package/src/server/responses/core.ts +39 -6
- package/src/web-search/loop.ts +10 -2
package/src/providers/quota.ts
CHANGED
|
@@ -1552,6 +1552,59 @@ type AccountQuotaCacheEntry = {
|
|
|
1552
1552
|
identity?: string;
|
|
1553
1553
|
isCurrent?: () => boolean;
|
|
1554
1554
|
};
|
|
1555
|
+
/** Expired measurements become unknown; missing reset evidence never implies a fresh allowance. */
|
|
1556
|
+
function normalizeAnthropicQuota(quota: ProviderQuota | null | undefined, now: number): ProviderQuota | null {
|
|
1557
|
+
if (!quota) return null;
|
|
1558
|
+
const validReset = (resetAt: unknown): resetAt is number => typeof resetAt === "number"
|
|
1559
|
+
&& Number.isFinite(resetAt) && resetAt > 0 && Number.isFinite(new Date(resetAt).getTime());
|
|
1560
|
+
let result = quota;
|
|
1561
|
+
for (const [percent, reset] of [
|
|
1562
|
+
["fiveHourPercent", "fiveHourResetAt"],
|
|
1563
|
+
["weeklyPercent", "weeklyResetAt"],
|
|
1564
|
+
["monthlyPercent", "monthlyResetAt"],
|
|
1565
|
+
] as const) {
|
|
1566
|
+
const resetAt = quota[reset];
|
|
1567
|
+
if (resetAt === undefined) continue;
|
|
1568
|
+
const valid = validReset(resetAt);
|
|
1569
|
+
if (valid && resetAt > now) continue;
|
|
1570
|
+
if (result === quota) result = { ...quota };
|
|
1571
|
+
if (valid) delete result[percent];
|
|
1572
|
+
delete result[reset];
|
|
1573
|
+
}
|
|
1574
|
+
// Persisted rows validate only the outer quota object, so custom data may be malformed.
|
|
1575
|
+
if (quota.customWindows !== undefined) {
|
|
1576
|
+
const windows = Array.isArray(quota.customWindows) ? quota.customWindows : [];
|
|
1577
|
+
const retained: ProviderQuotaWindow[] = [];
|
|
1578
|
+
let changed = !Array.isArray(quota.customWindows);
|
|
1579
|
+
for (const window of windows) {
|
|
1580
|
+
if (!window || typeof window !== "object" || typeof window.label !== "string" || !window.label.trim()
|
|
1581
|
+
|| typeof window.percent !== "number" || !Number.isFinite(window.percent)
|
|
1582
|
+
|| window.percent < 0 || window.percent > 100) {
|
|
1583
|
+
changed = true;
|
|
1584
|
+
continue;
|
|
1585
|
+
}
|
|
1586
|
+
if (validReset(window.resetAt) && window.resetAt <= now) {
|
|
1587
|
+
changed = true;
|
|
1588
|
+
continue;
|
|
1589
|
+
}
|
|
1590
|
+
if (window.resetAt !== undefined && !validReset(window.resetAt)) {
|
|
1591
|
+
const normalized = { ...window };
|
|
1592
|
+
delete normalized.resetAt;
|
|
1593
|
+
retained.push(normalized);
|
|
1594
|
+
changed = true;
|
|
1595
|
+
} else {
|
|
1596
|
+
retained.push(window);
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
if (changed) {
|
|
1600
|
+
if (result === quota) result = { ...quota };
|
|
1601
|
+
if (retained.length) result.customWindows = retained;
|
|
1602
|
+
else delete result.customWindows;
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
return hasQuotaRows(result) ? result : null;
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1555
1608
|
const accountQuotaCache = new Map<string, AccountQuotaCacheEntry>();
|
|
1556
1609
|
let explicitAccountEpoch = 0;
|
|
1557
1610
|
|
|
@@ -1568,14 +1621,23 @@ function hydrateAccountQuotaCache(): void {
|
|
|
1568
1621
|
if (diskHydrated) return;
|
|
1569
1622
|
diskHydrated = true;
|
|
1570
1623
|
for (const [key, quota] of readPersistedAccountQuotas()) {
|
|
1571
|
-
|
|
1624
|
+
// Disk stores observation time, not the Anthropic usage probe's clock.
|
|
1625
|
+
if (!accountQuotaCache.has(key)) {
|
|
1626
|
+
const anthropic = key.startsWith("anthropic\u0000");
|
|
1627
|
+
accountQuotaCache.set(key, {
|
|
1628
|
+
ts: anthropic ? 0 : quota.updatedAt,
|
|
1629
|
+
quota: anthropic ? normalizeAnthropicQuota(quota, Date.now()) : quota,
|
|
1630
|
+
});
|
|
1631
|
+
}
|
|
1572
1632
|
}
|
|
1573
1633
|
}
|
|
1574
1634
|
|
|
1575
1635
|
function persistAccountQuotaCache(): void {
|
|
1576
1636
|
schedulePersistAccountQuotas(function* () {
|
|
1637
|
+
const now = Date.now();
|
|
1577
1638
|
for (const [key, entry] of accountQuotaCache) {
|
|
1578
|
-
|
|
1639
|
+
const quota = key.startsWith("anthropic\u0000") ? normalizeAnthropicQuota(entry.quota, now) : entry.quota;
|
|
1640
|
+
if (quota) yield [key, quota] as [string, ProviderQuota];
|
|
1579
1641
|
}
|
|
1580
1642
|
});
|
|
1581
1643
|
}
|
|
@@ -1625,7 +1687,7 @@ function accountCacheKey(provider: string, accountId: string): string {
|
|
|
1625
1687
|
export function getCachedProviderAccountQuota(provider: string, accountId: string): ProviderQuota | null {
|
|
1626
1688
|
const entry = accountQuotaCache.get(accountCacheKey(provider, accountId));
|
|
1627
1689
|
if (entry?.isCurrent && !entry.isCurrent()) return null;
|
|
1628
|
-
return entry?.quota ?? null;
|
|
1690
|
+
return provider === "anthropic" ? normalizeAnthropicQuota(entry?.quota, Date.now()) : entry?.quota ?? null;
|
|
1629
1691
|
}
|
|
1630
1692
|
|
|
1631
1693
|
/** Test-only: seed or clear the per-account quota cache without probing upstream. */
|
|
@@ -1642,6 +1704,68 @@ export function setCachedProviderAccountQuotaForTests(
|
|
|
1642
1704
|
accountQuotaCache.set(key, { ts: Date.now(), quota });
|
|
1643
1705
|
}
|
|
1644
1706
|
|
|
1707
|
+
/** Unified headers report utilization fractions and epoch-second reset times. */
|
|
1708
|
+
function anthropicHeaderResetAt(value: string | null): number | undefined {
|
|
1709
|
+
const seconds = toFiniteNumber(value);
|
|
1710
|
+
if (seconds === undefined || seconds <= 0) return undefined;
|
|
1711
|
+
const timestamp = seconds * 1000;
|
|
1712
|
+
return Number.isFinite(new Date(timestamp).getTime()) ? timestamp : undefined;
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
export function parseAnthropicRateLimitHeaders(headers: Headers): ProviderQuota | null {
|
|
1716
|
+
const fiveHourPercent = normalizeUtilizationFraction(headers.get("anthropic-ratelimit-unified-5h-utilization"));
|
|
1717
|
+
const weeklyPercent = normalizeUtilizationFraction(headers.get("anthropic-ratelimit-unified-7d-utilization"));
|
|
1718
|
+
if (fiveHourPercent === undefined && weeklyPercent === undefined) return null;
|
|
1719
|
+
const fiveHourResetAt = anthropicHeaderResetAt(headers.get("anthropic-ratelimit-unified-5h-reset"));
|
|
1720
|
+
const weeklyResetAt = anthropicHeaderResetAt(headers.get("anthropic-ratelimit-unified-7d-reset"));
|
|
1721
|
+
return {
|
|
1722
|
+
...(fiveHourPercent !== undefined ? { fiveHourPercent } : {}),
|
|
1723
|
+
...(fiveHourPercent !== undefined && fiveHourResetAt !== undefined ? { fiveHourResetAt } : {}),
|
|
1724
|
+
...(weeklyPercent !== undefined ? { weeklyPercent } : {}),
|
|
1725
|
+
...(weeklyPercent !== undefined && weeklyResetAt !== undefined ? { weeklyResetAt } : {}),
|
|
1726
|
+
updatedAt: Date.now(),
|
|
1727
|
+
};
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
/** Reject unknown scales; round fraction conversion for persisted/displayed percentages. */
|
|
1731
|
+
function normalizeUtilizationFraction(value: string | null): number | undefined {
|
|
1732
|
+
const numeric = toFiniteNumber(value);
|
|
1733
|
+
if (numeric === undefined || numeric < 0 || numeric > 1) return undefined;
|
|
1734
|
+
return Math.round(numeric * 10_000) / 100;
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
/**
|
|
1738
|
+
* Merge serving-account observations without advancing the usage probe's clock or
|
|
1739
|
+
* erasing model-specific windows. The caller owns credential attribution; this guard
|
|
1740
|
+
* prevents a retired account key from being revived by an older config generation.
|
|
1741
|
+
*/
|
|
1742
|
+
export function recordAnthropicAccountQuotaFromHeaders(
|
|
1743
|
+
accountId: string,
|
|
1744
|
+
headers: Headers,
|
|
1745
|
+
writerGeneration: number,
|
|
1746
|
+
): void {
|
|
1747
|
+
if (!accountId) return;
|
|
1748
|
+
const observed = parseAnthropicRateLimitHeaders(headers);
|
|
1749
|
+
if (!observed) return;
|
|
1750
|
+
const key = accountCacheKey("anthropic", accountId);
|
|
1751
|
+
if (!mayCommitAccountQuotaKey(key, writerGeneration)) return;
|
|
1752
|
+
// Hydrate before writing, for the same reason `recordPassiveAccountQuota` does: this write
|
|
1753
|
+
// arrives unprompted from the request path, and `persistAccountQuotaCache` serializes the
|
|
1754
|
+
// whole map. Landing before any reader has hydrated would persist this single row and erase
|
|
1755
|
+
// every other provider's saved row.
|
|
1756
|
+
hydrateAccountQuotaCache();
|
|
1757
|
+
const previous = accountQuotaCache.get(key);
|
|
1758
|
+
accountQuotaCache.set(key, {
|
|
1759
|
+
...previous,
|
|
1760
|
+
// Headers do not prove that the last usage probe succeeded.
|
|
1761
|
+
ts: previous?.ts ?? 0,
|
|
1762
|
+
quota: normalizeAnthropicQuota({
|
|
1763
|
+
...normalizeAnthropicQuota(previous?.quota, observed.updatedAt), ...observed,
|
|
1764
|
+
}, observed.updatedAt),
|
|
1765
|
+
});
|
|
1766
|
+
persistAccountQuotaCache();
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1645
1769
|
/**
|
|
1646
1770
|
* Providers whose per-account quota is OBSERVED in-band, never probed.
|
|
1647
1771
|
*
|
|
@@ -1714,7 +1838,11 @@ export function readPassiveProviderAccountQuotas(provider: string): ProviderAcco
|
|
|
1714
1838
|
export function sweepExpiredProviderAccountQuotaRows(now = Date.now()): number {
|
|
1715
1839
|
let removed = 0;
|
|
1716
1840
|
for (const [key, entry] of accountQuotaCache) {
|
|
1717
|
-
|
|
1841
|
+
// Anthropic observations extend retention, never the usage probe's eligibility clock.
|
|
1842
|
+
const retainedAt = key.startsWith("anthropic\u0000")
|
|
1843
|
+
? Math.max(entry.ts, entry.quota?.updatedAt ?? 0)
|
|
1844
|
+
: entry.ts;
|
|
1845
|
+
if (retainedAt + ACCOUNT_QUOTA_TTL_MS > now) continue;
|
|
1718
1846
|
accountQuotaCache.delete(key);
|
|
1719
1847
|
removed += 1;
|
|
1720
1848
|
}
|
|
@@ -1907,10 +2035,13 @@ async function fetchAccountQuota(
|
|
|
1907
2035
|
): Promise<AccountQuotaCacheEntry> {
|
|
1908
2036
|
if (!supportsPerAccountQuota(provider)) return { ts: Date.now(), quota: null, unavailable: true };
|
|
1909
2037
|
if (explicitAccountReader(provider)) return fetchExplicitAccountQuota(provider, accountId, forceRefresh, providerConfig);
|
|
2038
|
+
if (provider === "anthropic") hydrateAccountQuotaCache();
|
|
1910
2039
|
const key = accountCacheKey(provider, accountId);
|
|
1911
2040
|
const writerGeneration = captureConfigGeneration();
|
|
1912
2041
|
const cached = accountQuotaCache.get(key);
|
|
1913
|
-
if (!forceRefresh && cached && Date.now() - cached.ts < ACCOUNT_QUOTA_TTL_MS)
|
|
2042
|
+
if (!forceRefresh && cached && Date.now() - cached.ts < ACCOUNT_QUOTA_TTL_MS) {
|
|
2043
|
+
return provider === "anthropic" ? { ...cached, quota: normalizeAnthropicQuota(cached.quota, Date.now()) } : cached;
|
|
2044
|
+
}
|
|
1914
2045
|
const joinable = accountQuotaInflight.get(key);
|
|
1915
2046
|
if (joinable) return joinable;
|
|
1916
2047
|
|
|
@@ -1947,7 +2078,9 @@ async function fetchAccountQuota(
|
|
|
1947
2078
|
// negative-cache instead of re-probing on every GUI poll.
|
|
1948
2079
|
const entry: AccountQuotaCacheEntry = {
|
|
1949
2080
|
ts: Date.now(),
|
|
1950
|
-
|
|
2081
|
+
// Settle once for all joiners against observations committed during the probe.
|
|
2082
|
+
quota: provider === "anthropic"
|
|
2083
|
+
? normalizeAnthropicQuota(accountQuotaCache.get(key)?.quota, Date.now()) : cached?.quota ?? null,
|
|
1951
2084
|
unavailable: true,
|
|
1952
2085
|
};
|
|
1953
2086
|
if (mayCommitAccountQuotaKey(key, writerGeneration)) {
|
|
@@ -1957,7 +2090,9 @@ async function fetchAccountQuota(
|
|
|
1957
2090
|
}
|
|
1958
2091
|
return entry;
|
|
1959
2092
|
}
|
|
1960
|
-
const entry: AccountQuotaCacheEntry = {
|
|
2093
|
+
const entry: AccountQuotaCacheEntry = {
|
|
2094
|
+
ts: Date.now(), quota: provider === "anthropic" ? normalizeAnthropicQuota(quota, Date.now()) : quota,
|
|
2095
|
+
};
|
|
1961
2096
|
if (mayCommitAccountQuotaKey(key, writerGeneration)) {
|
|
1962
2097
|
accountQuotaCache.set(key, entry);
|
|
1963
2098
|
// Exhaustion state rides the SAME commit guard as the quota row: a probe from a
|
|
@@ -1969,7 +2104,8 @@ async function fetchAccountQuota(
|
|
|
1969
2104
|
} catch {
|
|
1970
2105
|
const entry: AccountQuotaCacheEntry = {
|
|
1971
2106
|
ts: Date.now(),
|
|
1972
|
-
quota:
|
|
2107
|
+
quota: provider === "anthropic"
|
|
2108
|
+
? normalizeAnthropicQuota(accountQuotaCache.get(key)?.quota, Date.now()) : cached?.quota ?? null,
|
|
1973
2109
|
unavailable: true,
|
|
1974
2110
|
};
|
|
1975
2111
|
if (mayCommitAccountQuotaKey(key, writerGeneration)) {
|
|
@@ -2001,7 +2137,7 @@ export async function fetchProviderAccountQuotas(
|
|
|
2001
2137
|
const entry = await fetchAccountQuota(provider, account.id, forceRefresh, providerConfig);
|
|
2002
2138
|
const result: ProviderAccountQuota = {
|
|
2003
2139
|
accountId: account.id,
|
|
2004
|
-
quota: entry.quota,
|
|
2140
|
+
quota: provider === "anthropic" ? normalizeAnthropicQuota(entry.quota, Date.now()) : entry.quota,
|
|
2005
2141
|
...(entry.unavailable ? { unavailable: true as const } : {}),
|
|
2006
2142
|
};
|
|
2007
2143
|
if (!explicitAccountReader(provider)) return result;
|
|
@@ -2544,6 +2544,37 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
|
|
|
2544
2544
|
// yields an empty picker at runtime.
|
|
2545
2545
|
note: "Domestic BigModel Coding Plan endpoint (open.bigmodel.cn)",
|
|
2546
2546
|
},
|
|
2547
|
+
// Narrowed carry of #3641: the official Codex example declares a local static catalog,
|
|
2548
|
+
// not an HTTP /models contract. Keep Responses separate from the Chat endpoint above.
|
|
2549
|
+
// Source: https://docs.bigmodel.cn/cn/coding-plan/tool/codex.md (checked 2026-09-07).
|
|
2550
|
+
{
|
|
2551
|
+
id: "zhipu-bigmodel-responses",
|
|
2552
|
+
label: "Zhipu AI — BigModel Coding Plan (Responses)",
|
|
2553
|
+
baseUrl: "https://open.bigmodel.cn/api/v1",
|
|
2554
|
+
adapter: "openai-responses",
|
|
2555
|
+
authKind: "key",
|
|
2556
|
+
dashboardUrl: "https://bigmodel.cn/console/usercenter/apikeys",
|
|
2557
|
+
defaultModel: "glm-5.3",
|
|
2558
|
+
models: ["glm-5.3", "glm-5-turbo"],
|
|
2559
|
+
liveModels: false,
|
|
2560
|
+
// The local Codex catalog does not establish an authenticated HTTP /models contract.
|
|
2561
|
+
apiKeyValidation: "unknown",
|
|
2562
|
+
jawcodeBundle: "zai",
|
|
2563
|
+
// A pre-existing same-named custom provider must retain its destination and key boundary.
|
|
2564
|
+
preserveCustomDestination: true,
|
|
2565
|
+
modelContextWindows: { "glm-5.3": 1_048_576, "glm-5-turbo": 204_800 },
|
|
2566
|
+
modelInputModalities: { "glm-5.3": ["text"], "glm-5-turbo": ["text"] },
|
|
2567
|
+
modelReasoningEfforts: {
|
|
2568
|
+
"glm-5.3": ZAI_GLM_53_REASONING_EFFORTS,
|
|
2569
|
+
// Explicitly empty: Turbo must not inherit the generic selectable effort ladder.
|
|
2570
|
+
"glm-5-turbo": [],
|
|
2571
|
+
},
|
|
2572
|
+
modelDefaultReasoningEfforts: { "glm-5.3": "max", "glm-5-turbo": "max" },
|
|
2573
|
+
modelSupportsReasoningSummaries: { "glm-5.3": true, "glm-5-turbo": true },
|
|
2574
|
+
// Responses replay uses this provider-level flag, not the Chat-path model list.
|
|
2575
|
+
preserveResponsesReasoningContent: true,
|
|
2576
|
+
note: "Domestic BigModel Coding Plan Responses endpoint; static model roster",
|
|
2577
|
+
},
|
|
2547
2578
|
{ id: "nanogpt", label: "NanoGPT", baseUrl: "https://nano-gpt.com/api/v1", adapter: "openai-chat", authKind: "key", dashboardUrl: "https://nano-gpt.com/api" },
|
|
2548
2579
|
{ id: "synthetic", label: "Synthetic", baseUrl: "https://api.synthetic.new/openai/v1", adapter: "openai-chat", authKind: "key", dashboardUrl: "https://synthetic.new" },
|
|
2549
2580
|
// SiliconFlow publishes an OpenAI-compatible chat endpoint and a dynamic model catalog. Do not
|
package/src/responses/parser.ts
CHANGED
|
@@ -126,6 +126,12 @@ export function parseRequest(
|
|
|
126
126
|
}
|
|
127
127
|
return holder;
|
|
128
128
|
};
|
|
129
|
+
const preservePendingReplay = () => {
|
|
130
|
+
const replay = pendingReasoning.filter(entry => entry.envelopeSigned || entry.part.redacted?.length);
|
|
131
|
+
if (replay.length > 0) {
|
|
132
|
+
ensureAssistantPlaceholder(messages, data.model, now).content.push(...replay.map(entry => entry.part));
|
|
133
|
+
}
|
|
134
|
+
};
|
|
129
135
|
// Tool specs surfaced by a prior tool_search (deferred tools, e.g. subagents). Codex does not
|
|
130
136
|
// re-list these in `tools`, but chat models can only call listed tools — so we re-inject them.
|
|
131
137
|
const loadedToolSpecs: unknown[] = [];
|
|
@@ -148,6 +154,12 @@ export function parseRequest(
|
|
|
148
154
|
const effectiveType = (item as { type?: string }).type ?? ("role" in item ? "message" : undefined);
|
|
149
155
|
const itemRole = (item as { role?: string }).role;
|
|
150
156
|
const externalTaskInput = effectiveType === "function_call_output" ? externalTaskInputContent(item) : undefined;
|
|
157
|
+
// A signed/opaque assistant-only turn still owns its replay blocks, even
|
|
158
|
+
// without a following assistant text or tool call to drain the pending list.
|
|
159
|
+
if (effectiveType === "agent_message" || externalTaskInput !== undefined
|
|
160
|
+
|| (effectiveType === "message" && ["user", "developer", "system"].includes(itemRole ?? ""))) {
|
|
161
|
+
preservePendingReplay();
|
|
162
|
+
}
|
|
151
163
|
// Raw protocol items do not map one-to-one onto context messages. Capture the boundary while
|
|
152
164
|
// both representations are available so later metadata can stay before conversation in both.
|
|
153
165
|
if (
|
|
@@ -269,7 +281,7 @@ export function parseRequest(
|
|
|
269
281
|
const envelope = typeof reasoning.encrypted_content === "string"
|
|
270
282
|
? decodeReasoningEnvelope(reasoning.encrypted_content)
|
|
271
283
|
: null;
|
|
272
|
-
const thinkingText = envelope?.txt
|
|
284
|
+
const thinkingText = envelope?.txt ?? text;
|
|
273
285
|
|
|
274
286
|
// Kiro reasoning round-trip: a krc-only item carries nothing renderable — it is provider
|
|
275
287
|
// state for the assistant turn that ALREADY closed, because Kiro emits its
|
|
@@ -285,7 +297,7 @@ export function parseRequest(
|
|
|
285
297
|
|
|
286
298
|
// Native/non-ocxr1 encrypted-only reasoning is opaque here. Do not create a detached
|
|
287
299
|
// assistant turn or invent replayable plaintext/signatures from the encrypted payload.
|
|
288
|
-
if (thinkingText.length > 0) {
|
|
300
|
+
if (thinkingText.length > 0 || envelope?.sig || envelope?.red?.length) {
|
|
289
301
|
const part: OcxThinkingContent = {
|
|
290
302
|
type: "thinking",
|
|
291
303
|
thinking: thinkingText,
|
|
@@ -296,7 +308,7 @@ export function parseRequest(
|
|
|
296
308
|
const envelopeSigned = typeof envelope?.sig === "string";
|
|
297
309
|
const previous = pendingReasoning[pendingReasoning.length - 1];
|
|
298
310
|
|
|
299
|
-
if (!envelopeSigned && previous && !previous.envelopeSigned) {
|
|
311
|
+
if (!envelopeSigned && !part.redacted && previous && !previous.envelopeSigned && !previous.part.redacted) {
|
|
300
312
|
previous.part = {
|
|
301
313
|
...part,
|
|
302
314
|
thinking: `${previous.part.thinking}\n${part.thinking}`,
|
|
@@ -466,6 +478,7 @@ export function parseRequest(
|
|
|
466
478
|
}
|
|
467
479
|
}
|
|
468
480
|
}
|
|
481
|
+
preservePendingReplay();
|
|
469
482
|
if (data.previous_response_id && continuationConversationMessageIndex === undefined) {
|
|
470
483
|
continuationConversationMessageIndex = messages.length;
|
|
471
484
|
}
|
|
@@ -50,10 +50,11 @@ export function decodeReasoningEnvelope(encryptedContent: string): ReasoningEnve
|
|
|
50
50
|
if (red.length > 0) envelope.red = red;
|
|
51
51
|
}
|
|
52
52
|
const txt = (parsed as { txt?: unknown }).txt;
|
|
53
|
-
|
|
53
|
+
const hasTxt = typeof txt === "string";
|
|
54
|
+
if (hasTxt) envelope.txt = txt;
|
|
54
55
|
const krc = (parsed as { krc?: unknown }).krc;
|
|
55
56
|
if (typeof krc === "string" && krc.length > 0) envelope.krc = krc;
|
|
56
|
-
return envelope.sig || envelope.red ||
|
|
57
|
+
return envelope.sig || envelope.red || hasTxt || envelope.krc ? envelope : null;
|
|
57
58
|
} catch {
|
|
58
59
|
return null;
|
|
59
60
|
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { sseDataPayload, type SseBlockRewrite } from "./sse-payload-rewrite";
|
|
2
|
+
|
|
3
|
+
const GROK_CONTROL_FRAME_TYPES: Record<string, true> = {
|
|
4
|
+
"codex.rate_limits": true,
|
|
5
|
+
"codex.response.metadata": true,
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Hide Codex-only control frames from Grok's strict Responses decoder.
|
|
10
|
+
*
|
|
11
|
+
* The inspection branch still sees these frames before this client-facing
|
|
12
|
+
* rewrite, so quota accounting and response metadata remain available to the
|
|
13
|
+
* proxy while Grok receives only its declared Responses event variants.
|
|
14
|
+
*/
|
|
15
|
+
export function createGrokResponsesControlFrameBlockRewrite(): SseBlockRewrite {
|
|
16
|
+
return (block) => {
|
|
17
|
+
let eventName = "";
|
|
18
|
+
// SSE overwrites the event type on every event field, including empty resets.
|
|
19
|
+
// Like sseDataPayload, remove only one optional ASCII space after the colon.
|
|
20
|
+
for (const line of block.split(/\r?\n/)) {
|
|
21
|
+
if (line === "event") eventName = "";
|
|
22
|
+
else if (line.startsWith("event:")) {
|
|
23
|
+
const value = line.slice("event:".length);
|
|
24
|
+
eventName = value.startsWith(" ") ? value.slice(1) : value;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
if (GROK_CONTROL_FRAME_TYPES[eventName] === true) return [];
|
|
28
|
+
|
|
29
|
+
const payload = sseDataPayload(block);
|
|
30
|
+
if (payload === null || payload === "[DONE]") return [block];
|
|
31
|
+
|
|
32
|
+
let event: unknown;
|
|
33
|
+
try {
|
|
34
|
+
event = JSON.parse(payload);
|
|
35
|
+
} catch {
|
|
36
|
+
return [block];
|
|
37
|
+
}
|
|
38
|
+
if (!event || typeof event !== "object" || Array.isArray(event) || !("type" in event)) return [block];
|
|
39
|
+
return typeof event.type === "string" && GROK_CONTROL_FRAME_TYPES[event.type] === true
|
|
40
|
+
? []
|
|
41
|
+
: [block];
|
|
42
|
+
};
|
|
43
|
+
}
|
|
@@ -161,7 +161,7 @@ interface ClientIntegrationSyncOutcome {
|
|
|
161
161
|
}
|
|
162
162
|
|
|
163
163
|
/**
|
|
164
|
-
* Re-inject native clients that are switched ON and file
|
|
164
|
+
* Re-inject native clients that are switched ON and every file integration whose
|
|
165
165
|
* OpenCodex ownership record is the operator's durable opt-in.
|
|
166
166
|
*
|
|
167
167
|
* Only Codex used to run here, so a catalog change reached Codex and nothing else: a Grok
|
|
@@ -169,6 +169,10 @@ interface ClientIntegrationSyncOutcome {
|
|
|
169
169
|
* next `ocx start`. The startup path already gates each client on its own toggle
|
|
170
170
|
* (`src/cli/index.ts`), and this is that same fan-out for the on-demand command.
|
|
171
171
|
*
|
|
172
|
+
* File integrations use the catalog-refresh coordinator so owned blocks are
|
|
173
|
+
* updated without claiming unowned files. Aside remains on its multi-profile
|
|
174
|
+
* server-owned path inside that coordinator.
|
|
175
|
+
*
|
|
172
176
|
* A client that is OFF or never connected is omitted from the result rather than reported as skipped — the
|
|
173
177
|
* caller has to be able to tell "not touched" from "tried and failed". A client that fails
|
|
174
178
|
* does not fail the sync: Codex is the one that matters for routing, and a broken Grok file
|
|
@@ -233,7 +237,7 @@ export async function syncEnabledClientIntegrations(
|
|
|
233
237
|
},
|
|
234
238
|
config,
|
|
235
239
|
port,
|
|
236
|
-
}, ["mcode", "pi", "aside"]));
|
|
240
|
+
}, ["mcode", "pi", "aside", "raycast"]));
|
|
237
241
|
|
|
238
242
|
return out;
|
|
239
243
|
}
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
isIntegrationClientId,
|
|
23
23
|
type IntegrationClientId,
|
|
24
24
|
} from "../../integrations/registry";
|
|
25
|
+
import { detectRaycast, type RaycastInstall } from "../../integrations/raycast-detect";
|
|
25
26
|
import { readIntegrationState } from "../../integrations/state";
|
|
26
27
|
import { createIntegrationStateStore, type IntegrationStateStore } from "../../integrations/store";
|
|
27
28
|
import {
|
|
@@ -58,6 +59,13 @@ type RestoreResult = Awaited<ReturnType<typeof restoreIntegrationCoordinated>>;
|
|
|
58
59
|
|
|
59
60
|
export type IntegrationStateEnvelope = {
|
|
60
61
|
clientId: IntegrationClientId;
|
|
62
|
+
/**
|
|
63
|
+
* Raycast only, and only on the single-client read. Custom Providers is a
|
|
64
|
+
* Pro feature, so a file that is `current` can still be one Raycast ignores;
|
|
65
|
+
* this is the fact that lets status and the GUI say so. It is not part of
|
|
66
|
+
* the shared `IntegrationStatus`, which describes the file, not the app.
|
|
67
|
+
*/
|
|
68
|
+
raycast?: RaycastInstall;
|
|
61
69
|
} & IntegrationStateRecord;
|
|
62
70
|
|
|
63
71
|
export interface IntegrationStateListEnvelope {
|
|
@@ -141,6 +149,17 @@ export function setIntegrationPathTestHooks(hooks: { env?: NodeJS.ProcessEnv; ho
|
|
|
141
149
|
integrationPathTestHooks = hooks;
|
|
142
150
|
}
|
|
143
151
|
|
|
152
|
+
/**
|
|
153
|
+
* Raycast detection override for tests. The real detector spawns `defaults` and
|
|
154
|
+
* reads the developer's own subscription state, which is exactly the kind of
|
|
155
|
+
* host fact a route test must not depend on.
|
|
156
|
+
*/
|
|
157
|
+
let raycastDetectTestHook: (() => RaycastInstall) | null = null;
|
|
158
|
+
|
|
159
|
+
export function setRaycastDetectTestHook(hook: (() => RaycastInstall) | null): void {
|
|
160
|
+
raycastDetectTestHook = hook;
|
|
161
|
+
}
|
|
162
|
+
|
|
144
163
|
/** The `env`/`home` overrides, spread into every registry-resolving call. */
|
|
145
164
|
function pathOverrides(): { env?: NodeJS.ProcessEnv; home?: string } {
|
|
146
165
|
return {
|
|
@@ -177,7 +196,10 @@ export function setIntegrationMutationFlightTestHooks(
|
|
|
177
196
|
setIntegrationMutationFlightTestHook(hooks?.run ?? null);
|
|
178
197
|
// Path overrides are part of the same isolation contract: clearing flights
|
|
179
198
|
// while leaving a temp home bound would let the next suite write real files.
|
|
180
|
-
if (hooks === null)
|
|
199
|
+
if (hooks === null) {
|
|
200
|
+
integrationPathTestHooks = null;
|
|
201
|
+
raycastDetectTestHook = null;
|
|
202
|
+
}
|
|
181
203
|
}
|
|
182
204
|
|
|
183
205
|
/**
|
|
@@ -633,7 +655,12 @@ export async function handleIntegrationRoutes(ctx: ManagementContext): Promise<R
|
|
|
633
655
|
try {
|
|
634
656
|
const input = await buildIntegrationWriteInput(requestedClient, ctx, integrationStore());
|
|
635
657
|
const state = readIntegrationState(input);
|
|
636
|
-
|
|
658
|
+
// Detection runs only for the client that needs it: `defaults` is a
|
|
659
|
+
// process spawn, and no other client's read should pay for it.
|
|
660
|
+
const envelope: IntegrationStateEnvelope = requestedClient === "raycast"
|
|
661
|
+
? { ...state, raycast: (raycastDetectTestHook ?? detectRaycast)() }
|
|
662
|
+
: state;
|
|
663
|
+
return jsonResponse(envelope, 200, req, ctx.config);
|
|
637
664
|
} catch (error) {
|
|
638
665
|
return internalErrorResponse(error, ctx);
|
|
639
666
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { readFileSync } from "node:fs";
|
|
3
|
+
import { shouldInjectApiAuthHeader } from "../../codex/loopback-target";
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Codex parses a catalog entry's `input_modalities` as a closed enum, and one out-of-enum
|
|
@@ -480,6 +481,13 @@ export async function handleModelRoutes(ctx: ManagementContext): Promise<Respons
|
|
|
480
481
|
if (!(error instanceof ClientPathError)) throw error;
|
|
481
482
|
return jsonResponse({ error: error.message }, 400, req, config);
|
|
482
483
|
}
|
|
484
|
+
if (requested === "raycast" && shouldInjectApiAuthHeader(config)) {
|
|
485
|
+
return jsonResponse({
|
|
486
|
+
error: "Raycast export requires an unauthenticated loopback destination; this listener requires an admission header Raycast cannot supply.",
|
|
487
|
+
reason: "non_loopback",
|
|
488
|
+
}, 400, req, config);
|
|
489
|
+
}
|
|
490
|
+
const baseUrl = opencodeProxyBaseUrl(Number(url.port) || config.port, config.hostname, config);
|
|
483
491
|
let models: ExportModel[];
|
|
484
492
|
try {
|
|
485
493
|
// The ONE loader every export surface uses. It carries the visibility
|
|
@@ -499,7 +507,7 @@ export async function handleModelRoutes(ctx: ManagementContext): Promise<Respons
|
|
|
499
507
|
);
|
|
500
508
|
}
|
|
501
509
|
const built = buildClientConfigText(requested, {
|
|
502
|
-
baseUrl
|
|
510
|
+
baseUrl,
|
|
503
511
|
models,
|
|
504
512
|
config,
|
|
505
513
|
});
|
|
@@ -27,14 +27,39 @@ export class UnsupportedContentEncodingError extends Error {
|
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
export type BodySizeMeasurement =
|
|
31
|
+
| "declared_wire"
|
|
32
|
+
| "observed_wire_lower_bound"
|
|
33
|
+
| "decoded_exact"
|
|
34
|
+
| "decoded_lower_bound";
|
|
35
|
+
|
|
30
36
|
export class DecompressedBodyTooLargeError extends Error {
|
|
31
|
-
|
|
32
|
-
|
|
37
|
+
readonly measurement: BodySizeMeasurement | null;
|
|
38
|
+
|
|
39
|
+
constructor(
|
|
40
|
+
readonly bytes: number,
|
|
41
|
+
readonly limit: number = MAX_DECOMPRESSED_BODY_BYTES,
|
|
42
|
+
measurement: BodySizeMeasurement | null = null,
|
|
43
|
+
) {
|
|
44
|
+
// Legacy callers supply no provenance. Only fixed categories and finite
|
|
45
|
+
// numbers may reach the public message, including calls from untyped code.
|
|
46
|
+
const category = measurement === "declared_wire" || measurement === "observed_wire_lower_bound"
|
|
47
|
+
|| measurement === "decoded_exact" || measurement === "decoded_lower_bound"
|
|
48
|
+
? measurement : null;
|
|
49
|
+
const suffix = category !== null && Number.isFinite(bytes) && bytes >= 0
|
|
50
|
+
&& Number.isFinite(limit) && limit >= 0
|
|
51
|
+
? ` [measurement=${category}; bytes=${bytes}]` : "";
|
|
52
|
+
super(`Decompressed request body exceeds ${Number.isFinite(limit) ? limit : "unknown"} bytes${suffix}`);
|
|
53
|
+
this.measurement = category;
|
|
33
54
|
}
|
|
34
55
|
}
|
|
35
56
|
|
|
36
|
-
function assertBodySizeWithinLimit(
|
|
37
|
-
|
|
57
|
+
function assertBodySizeWithinLimit(
|
|
58
|
+
body: Uint8Array,
|
|
59
|
+
maxBytes: number,
|
|
60
|
+
measurement: BodySizeMeasurement = "decoded_exact",
|
|
61
|
+
): Uint8Array {
|
|
62
|
+
if (body.byteLength > maxBytes) throw new DecompressedBodyTooLargeError(body.byteLength, maxBytes, measurement);
|
|
38
63
|
return body;
|
|
39
64
|
}
|
|
40
65
|
|
|
@@ -112,7 +137,7 @@ async function readRequestBodyBytesCapped(
|
|
|
112
137
|
if (!value || value.byteLength === 0) continue;
|
|
113
138
|
|
|
114
139
|
if (value.byteLength > maxBytes - retainedBytes) {
|
|
115
|
-
const error = new DecompressedBodyTooLargeError(retainedBytes + value.byteLength, maxBytes);
|
|
140
|
+
const error = new DecompressedBodyTooLargeError(retainedBytes + value.byteLength, maxBytes, "observed_wire_lower_bound");
|
|
116
141
|
cancel(error);
|
|
117
142
|
throw error;
|
|
118
143
|
}
|
|
@@ -173,7 +198,8 @@ export function decodeRequestBody(
|
|
|
173
198
|
else throw new UnsupportedContentEncodingError(encoding);
|
|
174
199
|
} catch (err) {
|
|
175
200
|
if ((err as NodeJS.ErrnoException | null)?.code === "ERR_BUFFER_TOO_LARGE") {
|
|
176
|
-
|
|
201
|
+
// Inflation stopped at the cap; the full decoded size was never measured.
|
|
202
|
+
throw new DecompressedBodyTooLargeError(maxBytes + 1, maxBytes, "decoded_lower_bound");
|
|
177
203
|
}
|
|
178
204
|
throw err;
|
|
179
205
|
}
|
|
@@ -198,7 +224,7 @@ export async function readBoundedJsonRequestBody(
|
|
|
198
224
|
// Reject an honest oversized declaration before reading. Missing, malformed,
|
|
199
225
|
// and dishonest declarations remain bounded by the streaming reader below.
|
|
200
226
|
if (declaredLength !== null && declaredLength > maxBytes) {
|
|
201
|
-
const error = new DecompressedBodyTooLargeError(declaredLength, maxBytes);
|
|
227
|
+
const error = new DecompressedBodyTooLargeError(declaredLength, maxBytes, "declared_wire");
|
|
202
228
|
cancelStreamWithoutWaiting(req.body, error);
|
|
203
229
|
throw error;
|
|
204
230
|
}
|
|
@@ -211,7 +237,7 @@ export async function readBoundedJsonRequestBody(
|
|
|
211
237
|
} finally {
|
|
212
238
|
releaseReservation?.();
|
|
213
239
|
}
|
|
214
|
-
assertBodySizeWithinLimit(raw, maxBytes);
|
|
240
|
+
assertBodySizeWithinLimit(raw, maxBytes, "observed_wire_lower_bound");
|
|
215
241
|
const releaseRaw = budget?.observeAcceptedRequestCopy(raw.byteLength);
|
|
216
242
|
let releaseDecoded: (() => void) | undefined;
|
|
217
243
|
let releaseText: (() => void) | undefined;
|