@bitkyc08/opencodex 2.20.0 → 2.22.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/AGENTS_INSTALL.md +32 -0
- package/README.md +1 -1
- package/gui/dist/assets/{index-DSK3S5HY.js → index-ClEcVlFO.js} +43 -17
- package/gui/dist/assets/index-DQsMZzI5.css +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic.ts +2 -28
- package/src/adapters/google.ts +31 -3
- package/src/adapters/openai-chat.ts +25 -8
- package/src/adapters/responses-tool-schema.ts +67 -0
- package/src/bridge.ts +15 -2
- package/src/claude/agents-inject.ts +2 -2
- package/src/claude/gateway-cache.ts +41 -4
- package/src/cli/claude.ts +1 -1
- package/src/cli/codex-log-guard-doctor.ts +103 -0
- package/src/cli/dispatch.ts +7 -1
- package/src/cli/help.ts +1 -1
- package/src/cli/models.ts +16 -6
- package/src/cli/observe.ts +38 -2
- package/src/cli/registry.ts +2 -1
- package/src/cli/v2.ts +34 -1
- package/src/codex/app-server-processes.ts +46 -26
- package/src/codex/catalog/effort.ts +49 -1
- package/src/codex/catalog/parsing.ts +64 -4
- package/src/codex/catalog/provider-fetch.ts +12 -0
- package/src/codex/catalog/sync.ts +14 -1
- package/src/codex/convergence.ts +2 -0
- package/src/codex/inject.ts +3 -3
- package/src/codex/log-guard/inspect.ts +506 -0
- package/src/codex/log-guard/lock.ts +150 -0
- package/src/codex/log-guard/maintenance.ts +403 -0
- package/src/codex/log-guard/path-safety.ts +39 -0
- package/src/codex/log-guard/policy.ts +44 -0
- package/src/codex/log-guard/processes.ts +205 -0
- package/src/codex/log-guard/protection.ts +489 -0
- package/src/codex/log-guard/sqlite-errors.ts +9 -0
- package/src/codex/paths.ts +5 -0
- package/src/codex/plugins-doctor.ts +1 -1
- package/src/codex/project-config-warnings.ts +2 -2
- package/src/generated/compatibility-version.json +93 -45
- package/src/images/loop.ts +15 -5
- package/src/providers/antigravity-models.ts +11 -1
- package/src/providers/model-discovery.ts +94 -6
- package/src/providers/quota.ts +159 -0
- package/src/providers/registry.ts +20 -1
- package/src/providers/slug-codec.ts +29 -0
- package/src/responses/custom-tool-compat.ts +4 -1
- package/src/responses/parser.ts +7 -1
- package/src/responses/provider-opaque-metadata.ts +73 -0
- package/src/responses/schema.ts +6 -0
- package/src/router.ts +12 -4
- package/src/routing/capability.ts +32 -17
- package/src/server/auth-cors.ts +42 -6
- package/src/server/index.ts +1 -0
- package/src/server/management/agent-settings-routes.ts +20 -2
- package/src/server/management/context.ts +15 -0
- package/src/server/management/model-routes.ts +12 -3
- package/src/server/management/storage-log-guard-routes.ts +186 -0
- package/src/server/management-api.ts +3 -1
- package/src/server/responses/core.ts +13 -0
- package/src/server/system-env.ts +1 -1
- package/src/types.ts +24 -2
- package/src/web-search/loop.ts +21 -5
- package/gui/dist/assets/index-DF_UFrGS.css +0 -1
package/src/providers/quota.ts
CHANGED
|
@@ -38,6 +38,11 @@ const REQUEST_TIMEOUT_MS = 8_000;
|
|
|
38
38
|
export const QUOTA_RESPONSE_MAX_BYTES = 512 * 1024;
|
|
39
39
|
const KIMI_CODE_BASE_URL = "https://api.kimi.com/coding/v1";
|
|
40
40
|
const KIMI_CODE_USAGE_URL = `${KIMI_CODE_BASE_URL}/usages`;
|
|
41
|
+
const COMMAND_CODE_BASE_URL = "https://api.commandcode.ai";
|
|
42
|
+
const COMMAND_CODE_WHOAMI_URL = `${COMMAND_CODE_BASE_URL}/alpha/whoami`;
|
|
43
|
+
const COMMAND_CODE_CREDITS_URL = `${COMMAND_CODE_BASE_URL}/alpha/billing/credits`;
|
|
44
|
+
const COMMAND_CODE_SUBSCRIPTIONS_URL = `${COMMAND_CODE_BASE_URL}/alpha/billing/subscriptions`;
|
|
45
|
+
const COMMAND_CODE_USAGE_URL = `${COMMAND_CODE_BASE_URL}/alpha/usage/summary`;
|
|
41
46
|
const A6API_BASE_URL = "https://api.a6api.com";
|
|
42
47
|
const OPENCODE_GO_BASE_URL = "https://opencode.ai/zen/go/v1";
|
|
43
48
|
const OPENCODE_GO_USAGE_URL = `${OPENCODE_GO_BASE_URL}/usage`;
|
|
@@ -1510,6 +1515,12 @@ function isCanonicalKimiCodeBaseUrl(baseUrl: string): boolean {
|
|
|
1510
1515
|
return normalizedBaseUrl(baseUrl) === KIMI_CODE_BASE_URL;
|
|
1511
1516
|
}
|
|
1512
1517
|
|
|
1518
|
+
function isCanonicalCommandCodeBaseUrl(baseUrl: string): boolean {
|
|
1519
|
+
const normalized = normalizedBaseUrl(baseUrl);
|
|
1520
|
+
// OAuth preset points at the API root; the Provider-API preset at /provider/v1.
|
|
1521
|
+
return normalized === COMMAND_CODE_BASE_URL || normalized === `${COMMAND_CODE_BASE_URL}/provider/v1`;
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1513
1524
|
/** Prefer the nested `data` shell when the outer object is only an envelope. */
|
|
1514
1525
|
function unwrapKimiQuotaPayload(value: unknown): Record<string, unknown> | null {
|
|
1515
1526
|
const body = asRecord(value);
|
|
@@ -1631,6 +1642,145 @@ async function fetchKimiQuota(provider: string, config: OcxProviderConfig): Prom
|
|
|
1631
1642
|
return quota ? report(provider, "kimi:usages", quota) : null;
|
|
1632
1643
|
}
|
|
1633
1644
|
|
|
1645
|
+
/**
|
|
1646
|
+
* Command Code rolling window: `{ cap, used, resetAt }` off /alpha/billing/credits,
|
|
1647
|
+
* normalized to a percent with an optional reset timestamp.
|
|
1648
|
+
*/
|
|
1649
|
+
function parseCommandCodeWindow(value: unknown): { percent: number; resetAt?: number } | null {
|
|
1650
|
+
const row = asRecord(value);
|
|
1651
|
+
if (!row) return null;
|
|
1652
|
+
const cap = toFiniteNumber(row.cap);
|
|
1653
|
+
const used = toFiniteNumber(row.used);
|
|
1654
|
+
if (cap === undefined || used === undefined || cap <= 0 || used < 0) return null;
|
|
1655
|
+
const percent = normalizePercent((used / cap) * 100);
|
|
1656
|
+
if (percent === undefined) return null;
|
|
1657
|
+
const resetAt = quotaResetAt(row);
|
|
1658
|
+
return { percent, ...(resetAt !== undefined ? { resetAt } : {}) };
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
/** Soft-fail GET returning a parsed record, or null when unavailable. */
|
|
1662
|
+
async function fetchCommandCodeJson(url: string, bearer: string): Promise<Record<string, unknown> | null> {
|
|
1663
|
+
try {
|
|
1664
|
+
const response = await fetch(url, {
|
|
1665
|
+
headers: { Accept: "application/json", Authorization: `Bearer ${bearer}` },
|
|
1666
|
+
redirect: "error",
|
|
1667
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
1668
|
+
});
|
|
1669
|
+
if (!response.ok) return null;
|
|
1670
|
+
return asRecord(await readQuotaJson(response));
|
|
1671
|
+
} catch {
|
|
1672
|
+
return null;
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
/**
|
|
1677
|
+
* Soft-fail period spend (used) against the remaining credit pools → creditsUsd.
|
|
1678
|
+
* Period scoping: `since=<currentPeriodStart>` keeps spend aligned with the
|
|
1679
|
+
* pools' billing cycle, and `currentPeriodEnd` becomes expiresAt.
|
|
1680
|
+
*/
|
|
1681
|
+
async function fetchCommandCodeSpend(
|
|
1682
|
+
bearer: string,
|
|
1683
|
+
credits: Record<string, unknown> | null,
|
|
1684
|
+
orgQuery: string,
|
|
1685
|
+
): Promise<ProviderQuotaCreditsUsd | undefined> {
|
|
1686
|
+
if (!credits) return undefined;
|
|
1687
|
+
const subscriptionBody = await fetchCommandCodeJson(`${COMMAND_CODE_SUBSCRIPTIONS_URL}${orgQuery}`, bearer);
|
|
1688
|
+
const subscription = asRecord(subscriptionBody?.data) ?? subscriptionBody;
|
|
1689
|
+
const periodStart = typeof subscription?.currentPeriodStart === "string" ? subscription.currentPeriodStart.trim() : "";
|
|
1690
|
+
// Unscoped /usage/summary is lifetime spend; mixing it with current-cycle
|
|
1691
|
+
// remaining pools produces a wrong percent. Omit creditsUsd until a period exists.
|
|
1692
|
+
if (!periodStart) return undefined;
|
|
1693
|
+
const sinceQuery = `${orgQuery ? "&" : "?"}since=${encodeURIComponent(periodStart)}`;
|
|
1694
|
+
const expiresAt = normalizeResetAt(subscription?.currentPeriodEnd);
|
|
1695
|
+
const summaryBody = await fetchCommandCodeJson(`${COMMAND_CODE_USAGE_URL}${orgQuery}${sinceQuery}`, bearer);
|
|
1696
|
+
const summary = asRecord(summaryBody?.data) ?? summaryBody;
|
|
1697
|
+
const used = toFiniteNumber(summary?.totalCost) ?? toFiniteNumber(summary?.totalMonthlyCredits);
|
|
1698
|
+
if (used === undefined || used < 0) return undefined;
|
|
1699
|
+
const pools = [credits.monthlyCredits, credits.purchasedCredits, credits.freeCredits]
|
|
1700
|
+
.map(value => toFiniteNumber(value))
|
|
1701
|
+
.filter((value): value is number => value !== undefined);
|
|
1702
|
+
// Field presence is what separates a real balance from absent data: an exhausted
|
|
1703
|
+
// all-zero account still reports remaining=0, while no remaining-credit field at
|
|
1704
|
+
// all means there is nothing to meter.
|
|
1705
|
+
if (pools.length === 0) return undefined;
|
|
1706
|
+
const remaining = pools.reduce((sum, value) => sum + Math.max(0, value ?? 0), 0);
|
|
1707
|
+
const limit = used + remaining;
|
|
1708
|
+
const percent = normalizePercent(limit > 0 ? (used / limit) * 100 : 0);
|
|
1709
|
+
// Purchased credits roll over past the subscription period end, so an expiry is
|
|
1710
|
+
// only truthful when the aggregate contains no non-expiring purchased pool.
|
|
1711
|
+
const purchased = toFiniteNumber(credits.purchasedCredits) ?? 0;
|
|
1712
|
+
return percent === undefined
|
|
1713
|
+
? undefined
|
|
1714
|
+
: {
|
|
1715
|
+
used,
|
|
1716
|
+
limit,
|
|
1717
|
+
remaining,
|
|
1718
|
+
percent,
|
|
1719
|
+
...(expiresAt !== undefined && purchased <= 0 ? { expiresAt } : {}),
|
|
1720
|
+
};
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1723
|
+
/** OAuth access token or ACTIVE Provider-API key for the Command Code quota probe. */
|
|
1724
|
+
async function resolveCommandCodeQuotaBearer(config: OcxProviderConfig): Promise<string | null> {
|
|
1725
|
+
if (config.authMode === "oauth") {
|
|
1726
|
+
try {
|
|
1727
|
+
return await getValidAccessToken("command-code");
|
|
1728
|
+
} catch {
|
|
1729
|
+
return null;
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1732
|
+
// ACTIVE key only: a quota bar for a different account than the one routing
|
|
1733
|
+
// requests is a wrong meter, not a helpful one.
|
|
1734
|
+
return resolveEnvValue(config.apiKey)?.trim() || null;
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
/**
|
|
1738
|
+
* Command Code `GET /alpha/billing/credits` — the same Bearer surface the CLI's
|
|
1739
|
+
* usage view uses (windowLimits.fiveHour / windowLimits.weekly), plus soft
|
|
1740
|
+
* whoami (team orgId scoping) and subscription-scoped spend for creditsUsd.
|
|
1741
|
+
*/
|
|
1742
|
+
async function fetchCommandCodeQuota(provider: string, config: OcxProviderConfig): Promise<ProviderQuotaProbeResult> {
|
|
1743
|
+
// Never release credentials to a user-edited or lookalike provider host.
|
|
1744
|
+
if (!isCanonicalCommandCodeBaseUrl(config.baseUrl)) return null;
|
|
1745
|
+
const bearer = await resolveCommandCodeQuotaBearer(config);
|
|
1746
|
+
if (!bearer) return null;
|
|
1747
|
+
const whoamiBody = await fetchCommandCodeJson(COMMAND_CODE_WHOAMI_URL, bearer);
|
|
1748
|
+
const whoami = asRecord(whoamiBody?.data) ?? whoamiBody;
|
|
1749
|
+
const org = asRecord(whoami?.org);
|
|
1750
|
+
const orgId = typeof org?.id === "string" && org.id.trim() ? org.id.trim() : null;
|
|
1751
|
+
const orgQuery = orgId ? `?orgId=${encodeURIComponent(orgId)}` : "";
|
|
1752
|
+
const response = await fetch(`${COMMAND_CODE_CREDITS_URL}${orgQuery}`, {
|
|
1753
|
+
headers: { Accept: "application/json", Authorization: `Bearer ${bearer}` },
|
|
1754
|
+
redirect: "error",
|
|
1755
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
1756
|
+
});
|
|
1757
|
+
if (!response.ok) {
|
|
1758
|
+
return response.status >= 400 && response.status < 500 && response.status !== 408 && response.status !== 429
|
|
1759
|
+
? TERMINAL_QUOTA_FAILURE
|
|
1760
|
+
: null;
|
|
1761
|
+
}
|
|
1762
|
+
const raw = asRecord(await readQuotaJson(response));
|
|
1763
|
+
const body = asRecord(raw?.data) ?? raw;
|
|
1764
|
+
const credits = asRecord(body?.credits);
|
|
1765
|
+
const limits = asRecord(body?.windowLimits);
|
|
1766
|
+
if (!credits && !limits) return null;
|
|
1767
|
+
const fiveHour = parseCommandCodeWindow(limits?.fiveHour);
|
|
1768
|
+
const weekly = parseCommandCodeWindow(limits?.weekly);
|
|
1769
|
+
const creditsUsd = await fetchCommandCodeSpend(bearer, credits, orgQuery);
|
|
1770
|
+
return report(provider, "command-code:credits", {
|
|
1771
|
+
...(fiveHour ? {
|
|
1772
|
+
fiveHourPercent: fiveHour.percent,
|
|
1773
|
+
...(fiveHour.resetAt !== undefined ? { fiveHourResetAt: fiveHour.resetAt } : {}),
|
|
1774
|
+
} : {}),
|
|
1775
|
+
...(weekly ? {
|
|
1776
|
+
weeklyPercent: weekly.percent,
|
|
1777
|
+
...(weekly.resetAt !== undefined ? { weeklyResetAt: weekly.resetAt } : {}),
|
|
1778
|
+
} : {}),
|
|
1779
|
+
...(creditsUsd ? { creditsUsd } : {}),
|
|
1780
|
+
updatedAt: Date.now(),
|
|
1781
|
+
});
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1634
1784
|
/** Cursor included usage via api2.cursor.sh (Bearer from OAuth) — unofficial, may change. */
|
|
1635
1785
|
async function fetchCursorQuota(provider: string): Promise<ProviderQuotaReport | null> {
|
|
1636
1786
|
let accessToken: string;
|
|
@@ -1919,6 +2069,15 @@ async function maybeFetchProviderQuota(
|
|
|
1919
2069
|
if (provider.authMode === "key" && isCanonicalKimiCodeBaseUrl(provider.baseUrl)) {
|
|
1920
2070
|
return fetchKimiQuota(name, provider);
|
|
1921
2071
|
}
|
|
2072
|
+
// OAuth account login or Provider-API key only; forward/local modes carry no
|
|
2073
|
+
// credential of ours on the canonical host.
|
|
2074
|
+
if (provider.authMode === "oauth" && name === "command-code") {
|
|
2075
|
+
return fetchCommandCodeQuota(name, provider);
|
|
2076
|
+
}
|
|
2077
|
+
if ((provider.authMode ?? "key") === "key" && name === "commandcode"
|
|
2078
|
+
&& isCanonicalCommandCodeBaseUrl(provider.baseUrl)) {
|
|
2079
|
+
return fetchCommandCodeQuota(name, provider);
|
|
2080
|
+
}
|
|
1922
2081
|
if ((provider.authMode ?? "key") === "key" && name === "opencode-go") {
|
|
1923
2082
|
return fetchOpenCodeGoQuota(name, provider);
|
|
1924
2083
|
}
|
|
@@ -80,6 +80,11 @@ interface ProviderModelDiscoverySharedSpec {
|
|
|
80
80
|
maxResponseBytes?: number;
|
|
81
81
|
/** Optional lower raw-row ceiling; the process-wide hard ceiling still wins. */
|
|
82
82
|
maxModels?: number;
|
|
83
|
+
/**
|
|
84
|
+
* If a valid extracted id starts with this prefix, strip it and re-validate the remainder.
|
|
85
|
+
* Empty/invalid remainders skip that row only.
|
|
86
|
+
*/
|
|
87
|
+
stripIdPrefix?: string;
|
|
83
88
|
}
|
|
84
89
|
|
|
85
90
|
type ProviderModelDiscoveryLocation =
|
|
@@ -1977,7 +1982,11 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
|
|
|
1977
1982
|
// 260710 GLM-5.2 context and path-specific ids: Tier-2 evidence in
|
|
1978
1983
|
// devlog/_plan/260710_provider_hardening/002_research_cn.md.
|
|
1979
1984
|
// 260814: glm-5.3 / glm-5.3[1m] added per docs.z.ai/devpack/latest-model, which lists them as
|
|
1980
|
-
// Coding Plan ids on this same endpoint.
|
|
1985
|
+
// Coding Plan ids on this same endpoint.
|
|
1986
|
+
// 260815: docs.z.ai/guides/llm/glm-5.3 now publishes the capability table (thinking, streaming,
|
|
1987
|
+
// function calling, caching, structured output) and a 128K output budget, recorded here as the
|
|
1988
|
+
// exact 131_072 every other source in this repo uses for that model. Coding Plan pricing stays
|
|
1989
|
+
// unpublished, so no cost entry is asserted.
|
|
1981
1990
|
{
|
|
1982
1991
|
id: "zai", label: "Z.AI — GLM Coding Plan", baseUrl: "https://api.z.ai/api/coding/paas/v4", adapter: "openai-chat", authKind: "key",
|
|
1983
1992
|
dashboardUrl: "https://z.ai/manage-apikey/apikey-list", defaultModel: "glm-5.3",
|
|
@@ -1988,6 +1997,8 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
|
|
|
1988
1997
|
modelSuffixBracketStrip: true,
|
|
1989
1998
|
noVisionModels: ZAI_GLM_5X_MODELS,
|
|
1990
1999
|
modelReasoningEfforts: ZAI_GLM_5X_REASONING_EFFORTS,
|
|
2000
|
+
modelDefaultReasoningEfforts: Object.fromEntries(ZAI_GLM_53_MODELS.map(id => [id, "max"])),
|
|
2001
|
+
modelMaxOutputTokens: Object.fromEntries(ZAI_GLM_53_MODELS.map(id => [id, 131_072])),
|
|
1991
2002
|
modelSupportsReasoningSummaries: Object.fromEntries(ZAI_GLM_5X_MODELS.map(id => [id, true])),
|
|
1992
2003
|
preserveReasoningContentModels: ZAI_GLM_5X_MODELS,
|
|
1993
2004
|
},
|
|
@@ -2461,6 +2472,7 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
|
|
|
2461
2472
|
// Cloudflare Workers AI: OpenAI-compatible endpoint. The base URL contains {account_id}
|
|
2462
2473
|
// which must be resolved by the user at setup time. Model IDs use the @cf/ prefix.
|
|
2463
2474
|
// Live-verified 2026-07-21 against https://developers.cloudflare.com/workers-ai/models/
|
|
2475
|
+
// Official search is sibling to /ai/v1 (GET .../ai/models/search?format=openrouter).
|
|
2464
2476
|
id: "cloudflare-workers-ai", label: "Cloudflare Workers AI",
|
|
2465
2477
|
baseUrl: "https://api.cloudflare.com/client/v4/accounts/{account_id}/ai/v1",
|
|
2466
2478
|
adapter: "openai-chat", authKind: "key", freeTier: true,
|
|
@@ -2475,6 +2487,13 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
|
|
|
2475
2487
|
"@cf/zai-org/glm-5.2",
|
|
2476
2488
|
"@cf/mistralai/mistral-small-3.1-24b-instruct",
|
|
2477
2489
|
],
|
|
2490
|
+
liveModels: true,
|
|
2491
|
+
modelDiscovery: {
|
|
2492
|
+
path: "../models/search",
|
|
2493
|
+
query: { format: "openrouter", per_page: "1000" },
|
|
2494
|
+
stripIdPrefix: "workers-ai/",
|
|
2495
|
+
maxModels: 256,
|
|
2496
|
+
},
|
|
2478
2497
|
note: "Workers AI · Free tier included · Account ID required in base URL",
|
|
2479
2498
|
},
|
|
2480
2499
|
// FREEZE 2026-07-10: /models was auth-gated under key login. OAuth device-flow + copilot_internal
|
|
@@ -29,6 +29,20 @@ export function encodeRoutedModelId(id: string): string {
|
|
|
29
29
|
return id.includes("/") ? id.replaceAll("/", SLUG_ALIAS_SEPARATOR) : id;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
/**
|
|
33
|
+
* True when `modelId` shares a Codex-facing encoded form with a different known id.
|
|
34
|
+
* That collision is what makes `provider/openai-gpt-5.5` decode to native `openai-gpt-5.5`
|
|
35
|
+
* while a custom `openai/gpt-5.5` row is still visible.
|
|
36
|
+
*/
|
|
37
|
+
export function encodedModelIdCollides(modelId: string, knownIds: Iterable<string>): boolean {
|
|
38
|
+
const encoded = encodeRoutedModelId(modelId);
|
|
39
|
+
for (const id of knownIds) {
|
|
40
|
+
if (id === modelId) continue;
|
|
41
|
+
if (encodeRoutedModelId(id) === encoded) return true;
|
|
42
|
+
}
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
|
|
32
46
|
/** Codex-facing routed slug: exactly one "/" — `<provider>/<encoded id>`. */
|
|
33
47
|
export function routedSlug(provider: string, id: string): string {
|
|
34
48
|
return `${provider}/${encodeRoutedModelId(id)}`;
|
|
@@ -51,6 +65,21 @@ export function decodeRoutedModelId(requested: string, knownIds: Iterable<string
|
|
|
51
65
|
return aliasMatch ?? requested;
|
|
52
66
|
}
|
|
53
67
|
|
|
68
|
+
/**
|
|
69
|
+
* Decode a Codex-facing id, but fail when a custom slash id and another known id
|
|
70
|
+
* share the same encoded form. Write-time checks cannot cover a later live cache.
|
|
71
|
+
*/
|
|
72
|
+
export function decodeRoutedModelIdOrThrow(requested: string, knownIds: Iterable<string>): string {
|
|
73
|
+
const ids = [...knownIds];
|
|
74
|
+
const encodedRequested = encodeRoutedModelId(requested);
|
|
75
|
+
const matches = new Set<string>();
|
|
76
|
+
for (const id of ids) {
|
|
77
|
+
if (id === requested || encodeRoutedModelId(id) === encodedRequested) matches.add(id);
|
|
78
|
+
}
|
|
79
|
+
if (matches.size > 1) throw new Error(`ambiguous model id "${requested}"`);
|
|
80
|
+
return decodeRoutedModelId(requested, ids);
|
|
81
|
+
}
|
|
82
|
+
|
|
54
83
|
/** Does a stored config slug name this routed model, in either raw or encoded form? */
|
|
55
84
|
export function slugEquals(stored: string, provider: string, id: string): boolean {
|
|
56
85
|
return stored === `${provider}/${id}` || stored === routedSlug(provider, id);
|
|
@@ -70,6 +70,9 @@ function rewriteForUpstream(
|
|
|
70
70
|
|| isPlainObject(value.format)
|
|
71
71
|
|| isPlainObject(value.parameters);
|
|
72
72
|
if (!isDefinition) return { ...rest, type: "function" };
|
|
73
|
+
const inputDescription = value.name === "exec"
|
|
74
|
+
? "JavaScript source for unified exec. Use await tools.exec_command(...) for shell commands and text(...) to return textual output; do not provide a bare shell command."
|
|
75
|
+
: "Raw input for this client-executed custom tool.";
|
|
73
76
|
return {
|
|
74
77
|
...rest,
|
|
75
78
|
type: "function",
|
|
@@ -78,7 +81,7 @@ function rewriteForUpstream(
|
|
|
78
81
|
properties: {
|
|
79
82
|
input: {
|
|
80
83
|
type: "string",
|
|
81
|
-
description:
|
|
84
|
+
description: inputDescription,
|
|
82
85
|
},
|
|
83
86
|
},
|
|
84
87
|
required: ["input"],
|
package/src/responses/parser.ts
CHANGED
|
@@ -12,6 +12,7 @@ import type {
|
|
|
12
12
|
} from "../types";
|
|
13
13
|
import { namespacedToolName } from "../types";
|
|
14
14
|
import { responsesRequestSchema } from "./schema";
|
|
15
|
+
import { providerMetadataFromResponsesFunctionCall } from "./provider-opaque-metadata";
|
|
15
16
|
import { compactionItemToText } from "./compaction";
|
|
16
17
|
import { previousResponseReplayPrefixLength } from "./state";
|
|
17
18
|
import { decodeReasoningEnvelope } from "./reasoning-envelope";
|
|
@@ -498,7 +499,7 @@ export function parseRequest(body: unknown): OcxParsedRequest {
|
|
|
498
499
|
}
|
|
499
500
|
|
|
500
501
|
if (effectiveType === "function_call") {
|
|
501
|
-
const call = item as { id?: string; call_id: string; name: string; arguments?: string; namespace?: string };
|
|
502
|
+
const call = item as { id?: string; call_id: string; name: string; arguments?: string; namespace?: string; extra_content?: unknown };
|
|
502
503
|
// Tolerate empty/non-JSON arguments (e.g. a no-arg tool call serialized as "") instead of
|
|
503
504
|
// throwing — a single poisoned history item would otherwise 400 every subsequent turn.
|
|
504
505
|
let args: Record<string, unknown> = {};
|
|
@@ -519,6 +520,11 @@ export function parseRequest(body: unknown): OcxParsedRequest {
|
|
|
519
520
|
type: "toolCall", id: call.call_id, name: call.name, arguments: args,
|
|
520
521
|
...(call.namespace ? { namespace: call.namespace } : {}),
|
|
521
522
|
};
|
|
523
|
+
// Provider-opaque metadata (e.g. a Gemini thought signature) travels with the call so a
|
|
524
|
+
// history-replayed or previous_response_id turn rebuilds the same signed part instead of
|
|
525
|
+
// depending on the same-process replay cache (issue #1735).
|
|
526
|
+
const providerMetadata = providerMetadataFromResponsesFunctionCall(call);
|
|
527
|
+
if (providerMetadata) toolCall.providerMetadata = providerMetadata;
|
|
522
528
|
assistantHolderWithReasoning().content.push(toolCall);
|
|
523
529
|
continue;
|
|
524
530
|
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider-opaque tool-call metadata across the Responses boundary (issue #1735).
|
|
3
|
+
*
|
|
4
|
+
* Gemini issues a `thoughtSignature` on the exact part that carries a function call, and the
|
|
5
|
+
* next request is only valid if that signature comes back on the part rebuilt from that same
|
|
6
|
+
* call. Every synthetic loop in this proxy (web search, images, continuation replay) tears a
|
|
7
|
+
* tool call down into id/name/arguments and builds a fresh one, which silently dropped the
|
|
8
|
+
* signature and left only the same-process replay cache to paper over it. History replay and
|
|
9
|
+
* `previous_response_id` had no cache to fall back on.
|
|
10
|
+
*
|
|
11
|
+
* This module is the single seam where that metadata crosses into and out of the Responses
|
|
12
|
+
* wire, so a loop that rebuilds a call only has to carry one field instead of knowing about
|
|
13
|
+
* any provider. Values are treated as opaque: never parsed, merged, re-encoded, or synthesized.
|
|
14
|
+
*/
|
|
15
|
+
import type { OcxProviderOpaqueToolCallMetadata } from "../types";
|
|
16
|
+
|
|
17
|
+
/** Wire shape: `extra_content.google.thought_signature` on a Responses function_call item. */
|
|
18
|
+
interface ResponsesExtraContent {
|
|
19
|
+
google?: { thought_signature?: unknown };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function isObj(value: unknown): value is Record<string, unknown> {
|
|
23
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Same ceiling the Antigravity replay cache already enforces on a stored signature. An opaque
|
|
28
|
+
* token this large is not a real signature, and accepting it would let a caller push unbounded
|
|
29
|
+
* state through history replay.
|
|
30
|
+
*/
|
|
31
|
+
const MAX_SIGNATURE_BYTES = 64 * 1024;
|
|
32
|
+
|
|
33
|
+
function isCarryableSignature(value: unknown): value is string {
|
|
34
|
+
if (typeof value !== "string" || value.length === 0) return false;
|
|
35
|
+
// Cheap length pre-check: UTF-8 is at most 3 bytes per UTF-16 code unit for the BMP, so this
|
|
36
|
+
// skips the encode for the overwhelmingly common short case.
|
|
37
|
+
if (value.length <= MAX_SIGNATURE_BYTES / 3) return true;
|
|
38
|
+
return Buffer.byteLength(value, "utf8") <= MAX_SIGNATURE_BYTES;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Read provider metadata off an inbound Responses function_call item. */
|
|
42
|
+
export function providerMetadataFromResponsesFunctionCall(
|
|
43
|
+
item: { extra_content?: unknown } | undefined,
|
|
44
|
+
): OcxProviderOpaqueToolCallMetadata | undefined {
|
|
45
|
+
const extra = item?.extra_content;
|
|
46
|
+
if (!isObj(extra)) return undefined;
|
|
47
|
+
const google = (extra as ResponsesExtraContent).google;
|
|
48
|
+
if (!isObj(google)) return undefined;
|
|
49
|
+
const signature = google.thought_signature;
|
|
50
|
+
if (!isCarryableSignature(signature)) return undefined;
|
|
51
|
+
return { google: { thoughtSignature: signature } };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Serialize provider metadata onto an outbound Responses function_call item. */
|
|
55
|
+
export function responsesExtraContentFromProviderMetadata(
|
|
56
|
+
metadata: OcxProviderOpaqueToolCallMetadata | undefined,
|
|
57
|
+
): { extra_content: { google: { thought_signature: string } } } | undefined {
|
|
58
|
+
const signature = metadata?.google?.thoughtSignature;
|
|
59
|
+
if (!isCarryableSignature(signature)) return undefined;
|
|
60
|
+
return { extra_content: { google: { thought_signature: signature } } };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Copy metadata for a rebuilt tool call. A signature belongs to one specific part, so a loop
|
|
65
|
+
* that fans one model response into several calls must copy per call and never share or merge.
|
|
66
|
+
*/
|
|
67
|
+
export function cloneProviderOpaqueToolCallMetadata(
|
|
68
|
+
metadata: OcxProviderOpaqueToolCallMetadata | undefined,
|
|
69
|
+
): OcxProviderOpaqueToolCallMetadata | undefined {
|
|
70
|
+
const signature = metadata?.google?.thoughtSignature;
|
|
71
|
+
if (!isCarryableSignature(signature)) return undefined;
|
|
72
|
+
return { google: { thoughtSignature: signature } };
|
|
73
|
+
}
|
package/src/responses/schema.ts
CHANGED
|
@@ -64,6 +64,12 @@ const functionCallItemSchema = z.object({
|
|
|
64
64
|
name: z.string().min(1),
|
|
65
65
|
namespace: z.string().optional(),
|
|
66
66
|
arguments: z.string().optional(),
|
|
67
|
+
// Provider-opaque metadata that must survive the round trip verbatim (issue #1735). The shape
|
|
68
|
+
// is bounded on purpose: only the one nested key we round-trip is modeled, so an unexpected
|
|
69
|
+
// payload cannot ride through as arbitrary passthrough state.
|
|
70
|
+
extra_content: z.object({
|
|
71
|
+
google: z.object({ thought_signature: z.string().optional() }).optional(),
|
|
72
|
+
}).optional(),
|
|
67
73
|
});
|
|
68
74
|
const functionCallOutputItemSchema = z.object({
|
|
69
75
|
type: z.literal("function_call_output"),
|
package/src/router.ts
CHANGED
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
OPENAI_API_PROVIDER_ID,
|
|
25
25
|
OPENAI_CODEX_PROVIDER_ID,
|
|
26
26
|
} from "./providers/openai-tiers";
|
|
27
|
-
import {
|
|
27
|
+
import { decodeRoutedModelIdOrThrow, encodeRoutedModelId } from "./providers/slug-codec";
|
|
28
28
|
import { getStaleCached } from "./codex/model-cache";
|
|
29
29
|
import { codexAccountNamespaceEntries } from "./codex/account-namespaces";
|
|
30
30
|
import {
|
|
@@ -87,9 +87,14 @@ const MODEL_PROVIDER_PATTERNS: Array<{ providerNames: string[]; prefixes: string
|
|
|
87
87
|
* last-known-good live /models cache (may be empty on a cold start; decode then passes
|
|
88
88
|
* unknown ids through unchanged for an honest upstream error).
|
|
89
89
|
*/
|
|
90
|
-
export function knownModelIdsForProvider(
|
|
90
|
+
export function knownModelIdsForProvider(
|
|
91
|
+
provName: string,
|
|
92
|
+
prov: OcxProviderConfig,
|
|
93
|
+
config?: Pick<OcxConfig, "customModels">,
|
|
94
|
+
): string[] {
|
|
91
95
|
const ids = new Set<string>();
|
|
92
96
|
for (const id of prov.models ?? []) ids.add(id);
|
|
97
|
+
if (prov.defaultModel) ids.add(prov.defaultModel);
|
|
93
98
|
const registry = providerMatchesRegistryTransportWithStaticGuards(provName, prov)
|
|
94
99
|
? PROVIDER_REGISTRY.find(entry => entry.id === provName)
|
|
95
100
|
: undefined;
|
|
@@ -108,6 +113,9 @@ export function knownModelIdsForProvider(provName: string, prov: OcxProviderConf
|
|
|
108
113
|
for (const id of Object.keys(map ?? {})) ids.add(id);
|
|
109
114
|
}
|
|
110
115
|
for (const cached of getStaleCached(provName) ?? []) ids.add(cached.id);
|
|
116
|
+
for (const model of config?.customModels ?? []) {
|
|
117
|
+
if (model.provider === provName && model.modelId) ids.add(model.modelId);
|
|
118
|
+
}
|
|
111
119
|
return [...ids];
|
|
112
120
|
}
|
|
113
121
|
|
|
@@ -610,7 +618,7 @@ function routeModelInternal(
|
|
|
610
618
|
if (hasOwnProvider(config.providers, provName)) {
|
|
611
619
|
const prov = config.providers[provName];
|
|
612
620
|
if (prov.disabled === true) throw new Error(`Provider is disabled: ${provName}`);
|
|
613
|
-
const known = knownModelIdsForProvider(provName, prov);
|
|
621
|
+
const known = knownModelIdsForProvider(provName, prov, config);
|
|
614
622
|
// Self-namespaced native id — the vendor segment equals the provider id, so the FULL ref is
|
|
615
623
|
// itself a known model (e.g. orcarouter/auto). Route it whole instead of stripping to the
|
|
616
624
|
// remainder, which would send a bare `auto` the upstream cannot resolve.
|
|
@@ -622,7 +630,7 @@ function routeModelInternal(
|
|
|
622
630
|
return routeResult(
|
|
623
631
|
provName,
|
|
624
632
|
prov,
|
|
625
|
-
|
|
633
|
+
decodeRoutedModelIdOrThrow(modelId.slice(slash + 1), known),
|
|
626
634
|
"explicit-provider",
|
|
627
635
|
"explicit-provider-namespace",
|
|
628
636
|
);
|
|
@@ -26,11 +26,12 @@ import { statSync } from "node:fs";
|
|
|
26
26
|
import type { RouteCapabilityEvidence } from "./trace";
|
|
27
27
|
|
|
28
28
|
type CatalogModelRow = {
|
|
29
|
+
/** Exact provider/native-id identity, from the provenance block. */
|
|
29
30
|
provider: string;
|
|
30
31
|
id: string;
|
|
32
|
+
/** Only values a real source asserted; never a strict-parser default. */
|
|
31
33
|
contextWindow?: number;
|
|
32
34
|
inputModalities?: string[];
|
|
33
|
-
reasoningEfforts?: string[];
|
|
34
35
|
capabilities?: string[];
|
|
35
36
|
};
|
|
36
37
|
|
|
@@ -52,23 +53,33 @@ function cachedCatalogModels(): CatalogModelRow[] {
|
|
|
52
53
|
const catalog = readCatalog(path);
|
|
53
54
|
const models = catalog?.models;
|
|
54
55
|
if (!Array.isArray(models)) return [];
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
56
|
+
// Read ONLY `opencodex_capability_provenance` (written by
|
|
57
|
+
// applyCatalogModelMetadata). The row's own `context_window` and
|
|
58
|
+
// `input_modalities` always exist because ensureStrictCatalogFields fills them
|
|
59
|
+
// with compatibility defaults for Codex's strict parser, so reading them would
|
|
60
|
+
// turn "nobody asserted anything" into a confident `image: false` and a
|
|
61
|
+
// fabricated 128000 — the opposite of this module's contract. A row without
|
|
62
|
+
// provenance contributes nothing.
|
|
63
|
+
const rows = models.flatMap((model): CatalogModelRow[] => {
|
|
64
|
+
if (typeof model !== "object" || model === null) return [];
|
|
65
|
+
const provenance = (model as Record<string, unknown>).opencodex_capability_provenance;
|
|
66
|
+
if (typeof provenance !== "object" || provenance === null) return [];
|
|
67
|
+
const source = provenance as Record<string, unknown>;
|
|
68
|
+
if (typeof source.provider !== "string" || typeof source.model_id !== "string") return [];
|
|
69
|
+
return [{
|
|
70
|
+
provider: source.provider,
|
|
71
|
+
id: source.model_id,
|
|
72
|
+
...(typeof source.context_window === "number" && source.context_window > 0
|
|
73
|
+
? { contextWindow: source.context_window }
|
|
64
74
|
: {}),
|
|
65
|
-
...(Array.isArray(
|
|
66
|
-
? {
|
|
75
|
+
...(Array.isArray(source.input_modalities)
|
|
76
|
+
? { inputModalities: source.input_modalities.filter((value): value is string => typeof value === "string") }
|
|
67
77
|
: {}),
|
|
68
|
-
...(Array.isArray(
|
|
69
|
-
? { capabilities:
|
|
78
|
+
...(Array.isArray(source.capabilities)
|
|
79
|
+
? { capabilities: source.capabilities.filter((value): value is string => typeof value === "string") }
|
|
70
80
|
: {}),
|
|
71
|
-
}
|
|
81
|
+
}];
|
|
82
|
+
});
|
|
72
83
|
catalogCache = { path, mtimeMs, rows };
|
|
73
84
|
return rows;
|
|
74
85
|
} catch {
|
|
@@ -177,13 +188,17 @@ export function candidateCapabilityEvidence(
|
|
|
177
188
|
// override.
|
|
178
189
|
const tools = capabilities.includes("tools")
|
|
179
190
|
|| isNative
|
|
180
|
-
|
|
191
|
+
// The adapter protocol is positive evidence on its own. This was once gated
|
|
192
|
+
// on `catalogRow === undefined`, which was only safe while the catalog lookup
|
|
193
|
+
// never matched anything: once it matches, a row that simply does not
|
|
194
|
+
// enumerate "tools" would silently revoke tool support for every openai-chat
|
|
195
|
+
// and anthropic candidate.
|
|
196
|
+
|| (provider !== undefined && TOOL_CAPABLE_ADAPTERS.has(provider.adapter))
|
|
181
197
|
|| provider?.parallelToolCalls === true
|
|
182
198
|
|| undefined;
|
|
183
199
|
|
|
184
200
|
const reasoningEfforts = provider?.modelReasoningEfforts?.[modelId]
|
|
185
201
|
?? registryEntry?.modelReasoningEfforts?.[modelId]
|
|
186
|
-
?? catalogRow?.reasoningEfforts
|
|
187
202
|
?? (isNative ? nativeReasoningEfforts(modelId) : undefined);
|
|
188
203
|
|
|
189
204
|
const tierSupport = provider
|
package/src/server/auth-cors.ts
CHANGED
|
@@ -140,17 +140,53 @@ export function browserSecurityHeaders(): Record<string, string> {
|
|
|
140
140
|
};
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
+
/**
|
|
144
|
+
* Baseline data-plane request headers. ChatGPT-Account-Id is required for browser/Electron
|
|
145
|
+
* ChatGPT & Codex App voice preflights (direct forward auth matches the bearer to this account
|
|
146
|
+
* id). The OpenAI-Alpha .. X-OAI-Attestation block covers GPT-Live voice protocol headers
|
|
147
|
+
* relayed by the /v1/live call-create path.
|
|
148
|
+
*/
|
|
149
|
+
const STATIC_ALLOWED_REQUEST_HEADERS =
|
|
150
|
+
"Content-Type, Authorization, X-OpenCodex-API-Key, X-Api-Key, Anthropic-Version, Anthropic-Beta, ChatGPT-Account-Id, OpenAI-Alpha, X-Session-Id, Session-Id, Thread-Id, Originator, X-OAI-Attestation";
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* A fixed allow-list cannot enumerate vendor telemetry headers: the OpenAI and Anthropic
|
|
154
|
+
* browser SDKs send `X-Stainless-*` describing runtime and retry state, and the browser blocks
|
|
155
|
+
* the real request when the preflight omits even one of them (#1773).
|
|
156
|
+
*
|
|
157
|
+
* Echo what an already-allowed origin asked for, and fall back to the static list otherwise.
|
|
158
|
+
* The echo is deliberately gated on the origin check that ran first: this widens which headers
|
|
159
|
+
* an admitted caller may send, never which origins are admitted, and it grants nothing to an
|
|
160
|
+
* origin that would have been rejected anyway. Authentication is unchanged — the preflight
|
|
161
|
+
* itself carries no credential and produces no auth or account-pool side effect.
|
|
162
|
+
*/
|
|
163
|
+
function allowedRequestHeaders(req?: Request): string {
|
|
164
|
+
const requested = req?.headers.get("Access-Control-Request-Headers")?.trim();
|
|
165
|
+
if (!requested) return STATIC_ALLOWED_REQUEST_HEADERS;
|
|
166
|
+
const seen = new Set(STATIC_ALLOWED_REQUEST_HEADERS.split(",").map(h => h.trim().toLowerCase()));
|
|
167
|
+
const extra: string[] = [];
|
|
168
|
+
for (const raw of requested.split(",")) {
|
|
169
|
+
const name = raw.trim();
|
|
170
|
+
// Header names are case-insensitive on the wire, so normalize before de-duplicating;
|
|
171
|
+
// echo the caller's spelling for the ones we add.
|
|
172
|
+
if (!name || seen.has(name.toLowerCase())) continue;
|
|
173
|
+
seen.add(name.toLowerCase());
|
|
174
|
+
extra.push(name);
|
|
175
|
+
}
|
|
176
|
+
return extra.length === 0 ? STATIC_ALLOWED_REQUEST_HEADERS : `${STATIC_ALLOWED_REQUEST_HEADERS}, ${extra.join(", ")}`;
|
|
177
|
+
}
|
|
178
|
+
|
|
143
179
|
export function corsHeaders(req?: Request, config?: RequestPolicyView): Record<string, string> {
|
|
144
180
|
const origin = req?.headers.get("Origin");
|
|
145
|
-
const
|
|
181
|
+
const originAllowed = Boolean(origin && req && config && isAllowedRequestOrigin(req, config));
|
|
182
|
+
const allowOrigin = originAllowed && origin ? origin : _corsOrigin;
|
|
146
183
|
return {
|
|
147
184
|
"Access-Control-Allow-Origin": allowOrigin,
|
|
148
185
|
"Access-Control-Allow-Methods": "GET, POST, PUT, PATCH, DELETE, OPTIONS",
|
|
149
|
-
|
|
150
|
-
//
|
|
151
|
-
//
|
|
152
|
-
"
|
|
153
|
-
"Vary": "Origin",
|
|
186
|
+
"Access-Control-Allow-Headers": allowedRequestHeaders(originAllowed ? req : undefined),
|
|
187
|
+
// A response that varies by the request's headers must say so, or a shared cache can
|
|
188
|
+
// replay one client's allow-list to a client that asked for different headers.
|
|
189
|
+
"Vary": "Origin, Access-Control-Request-Headers",
|
|
154
190
|
...browserSecurityHeaders(),
|
|
155
191
|
};
|
|
156
192
|
}
|
package/src/server/index.ts
CHANGED
|
@@ -988,6 +988,7 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
988
988
|
providerContextCap(config, OPENAI_CODEX_PROVIDER_ID),
|
|
989
989
|
accountNativeSlugs,
|
|
990
990
|
accountNativeSlugsBySelector,
|
|
991
|
+
config.keepNativeChatGptOnV1 === true,
|
|
991
992
|
);
|
|
992
993
|
return jsonResponse({
|
|
993
994
|
models: applyNativeVisibility(
|