@bitkyc08/opencodex 2.6.4 → 2.6.5

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.
@@ -16,7 +16,7 @@
16
16
  } catch (e) {}
17
17
  })();
18
18
  </script>
19
- <script type="module" crossorigin src="/assets/index-MnDRzA4G.js"></script>
19
+ <script type="module" crossorigin src="/assets/index-DcMEJXTd.js"></script>
20
20
  <link rel="stylesheet" crossorigin href="/assets/index-BwvDb198.css">
21
21
  </head>
22
22
  <body>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bitkyc08/opencodex",
3
- "version": "2.6.4",
3
+ "version": "2.6.5",
4
4
  "description": "Universal provider proxy for OpenAI Codex — use any LLM with Codex CLI/App/SDK",
5
5
  "type": "module",
6
6
  "main": "./bin/package-main.mjs",
@@ -70,7 +70,7 @@ async function normalizeFinalKiroHttpError(res: Response): Promise<Response> {
70
70
  }
71
71
 
72
72
  export async function fetchKiroWithRetry(request: AdapterRequest, ctx: AdapterFetchContext = {}): Promise<Response> {
73
- const timeoutMs = ctx.timeoutMs ?? 30_000;
73
+ const timeoutMs = ctx.timeoutMs ?? 100_000;
74
74
  let lastError: unknown;
75
75
  for (let attempt = 0; attempt < KIRO_RETRY_ATTEMPTS; attempt++) {
76
76
  if (ctx.abortSignal?.aborted) throw abortError(ctx.abortSignal);
@@ -10,6 +10,7 @@ import type { OcxConfig, OcxProviderConfig } from "./types";
10
10
  import { CODEX_REASONING_LEVELS, configuredReasoningEfforts, modelRecordValue, sanitizeCodexReasoningEfforts } from "./reasoning-effort";
11
11
  import { getJawcodeModelMetadata, getJawcodeModelMetadataCaseInsensitive, listJawcodeModelMetadata, resolveJawcodeProvider } from "./generated/jawcode-model-metadata";
12
12
  import { shouldCaseFoldMetadataModelId } from "./providers/derive";
13
+ import { applyProviderContextCap, providerContextCap } from "./provider-context-cap";
13
14
 
14
15
  const BUNDLED_CATALOG_CACHE_MS = 60_000;
15
16
  let bundledCatalogCache: { expiresAt: number; value: RawCatalog | null } | null = null;
@@ -81,7 +82,16 @@ export function nativeOpenAiSlugs(): string[] {
81
82
  return live.length > 0 ? unique([...live, ...DOCUMENTED_NATIVE_OPENAI_ADDITIONS]) : NATIVE_OPENAI_MODELS;
82
83
  }
83
84
 
84
- export interface CatalogModel { id: string; provider: string; owned_by?: string; reasoningEfforts?: string[]; contextWindow?: number; inputModalities?: string[]; }
85
+ export interface CatalogModel {
86
+ id: string;
87
+ provider: string;
88
+ owned_by?: string;
89
+ reasoningEfforts?: string[];
90
+ contextWindow?: number;
91
+ contextCap?: number;
92
+ contextCapped?: boolean;
93
+ inputModalities?: string[];
94
+ }
85
95
  type RawEntry = Record<string, unknown>;
86
96
  type RawCatalog = { models?: RawEntry[]; [k: string]: unknown };
87
97
  const JAWCODE_CATALOG_AUGMENT_PROVIDERS = new Set(["opencode-go"]);
@@ -230,7 +240,7 @@ export function normalizeRoutedCatalogEntry(entry: RawEntry): RawEntry {
230
240
  return ensureStrictCatalogFields(entry);
231
241
  }
232
242
 
233
- function applyJawcodeCatalogMetadata(entry: RawEntry, slug: string): void {
243
+ function applyJawcodeCatalogMetadata(entry: RawEntry, slug: string, contextCap?: number): void {
234
244
  const slash = slug.indexOf("/");
235
245
  if (slash < 0) return;
236
246
  const provider = slug.slice(0, slash);
@@ -241,9 +251,10 @@ function applyJawcodeCatalogMetadata(entry: RawEntry, slug: string): void {
241
251
  ?? (shouldCaseFoldMetadataModelId(provider) ? getJawcodeModelMetadataCaseInsensitive(jawcodeProvider, modelId) : undefined);
242
252
  if (!meta) return;
243
253
  if (typeof meta.contextWindow === "number" && meta.contextWindow > 0) {
244
- entry.context_window = meta.contextWindow;
245
- entry.max_context_window = meta.contextWindow;
246
- entry.auto_compact_token_limit = Math.floor(meta.contextWindow * 0.9);
254
+ const contextWindow = applyProviderContextCap(meta.contextWindow, contextCap) ?? meta.contextWindow;
255
+ entry.context_window = contextWindow;
256
+ entry.max_context_window = contextWindow;
257
+ entry.auto_compact_token_limit = Math.floor(contextWindow * 0.9);
247
258
  }
248
259
  if (Array.isArray(meta.input) && meta.input.length > 0) {
249
260
  entry.input_modalities = meta.input;
@@ -439,7 +450,7 @@ function deriveEntry(template: RawEntry | null, slug: string, desc: string, prio
439
450
  }
440
451
  applyReasoningLevels(e, model?.reasoningEfforts);
441
452
  normalizeRoutedCatalogEntry(e);
442
- applyJawcodeCatalogMetadata(e, slug);
453
+ applyJawcodeCatalogMetadata(e, slug, model?.contextCap);
443
454
  applyCatalogModelMetadata(e, model);
444
455
  } else {
445
456
  applyNativeOpenAiContextOverride(e);
@@ -455,7 +466,7 @@ function deriveEntry(template: RawEntry | null, slug: string, desc: string, prio
455
466
  };
456
467
  if (slug.includes("/")) applyReasoningLevels(entry, model?.reasoningEfforts);
457
468
  else applyReasoningLevels(entry);
458
- applyJawcodeCatalogMetadata(entry, slug);
469
+ applyJawcodeCatalogMetadata(entry, slug, model?.contextCap);
459
470
  applyCatalogModelMetadata(entry, model);
460
471
  applyNativeOpenAiContextOverride(entry);
461
472
  return ensureStrictCatalogFields(normalizeServiceTiers(entry));
@@ -577,33 +588,38 @@ function configuredInputModalities(prov: OcxProviderConfig, id: string): string[
577
588
  return Array.isArray(modalities) && modalities.length > 0 ? [...modalities] : undefined;
578
589
  }
579
590
 
580
- function applyProviderConfigHints(name: string, prov: OcxProviderConfig, model: CatalogModel): CatalogModel {
591
+ function applyProviderConfigHints(name: string, prov: OcxProviderConfig, model: CatalogModel, providerCap?: number): CatalogModel {
581
592
  void name;
582
- const contextCap = configuredContextWindow(prov, model.id);
593
+ const configuredCap = configuredContextWindow(prov, model.id);
583
594
  const inputModalities = configuredInputModalities(prov, model.id);
584
595
  const reasoningEfforts = configuredReasoningEfforts(prov, model.id);
585
- return {
596
+ const hinted = {
586
597
  ...model,
587
- ...(contextCap !== undefined
598
+ ...(configuredCap !== undefined
588
599
  ? {
589
600
  contextWindow: typeof model.contextWindow === "number" && model.contextWindow > 0
590
- ? Math.min(model.contextWindow, contextCap)
591
- : contextCap,
601
+ ? Math.min(model.contextWindow, configuredCap)
602
+ : configuredCap,
592
603
  }
593
604
  : {}),
594
605
  ...(inputModalities ? { inputModalities } : {}),
595
606
  ...(reasoningEfforts !== undefined ? { reasoningEfforts } : {}),
596
607
  };
608
+ const capped = applyProviderContextCap(hinted.contextWindow, providerCap);
609
+ if (providerCap !== undefined && capped !== hinted.contextWindow) {
610
+ return { ...hinted, contextWindow: capped, contextCap: providerCap, contextCapped: true };
611
+ }
612
+ return providerCap !== undefined ? { ...hinted, contextCap: providerCap, contextCapped: false } : hinted;
597
613
  }
598
614
 
599
- function catalogHintsFromProviderConfig(name: string, prov: OcxProviderConfig, id: string): Partial<CatalogModel> {
600
- const hinted = applyProviderConfigHints(name, prov, { id, provider: name });
615
+ function catalogHintsFromProviderConfig(name: string, prov: OcxProviderConfig, id: string, contextCap?: number): Partial<CatalogModel> {
616
+ const hinted = applyProviderConfigHints(name, prov, { id, provider: name }, contextCap);
601
617
  const { provider: _provider, id: _id, ...hints } = hinted;
602
618
  return hints;
603
619
  }
604
620
 
605
- function applyConfigHintsToCachedModels(name: string, prov: OcxProviderConfig, models: CatalogModel[]): CatalogModel[] {
606
- return models.map(model => applyProviderConfigHints(name, prov, model));
621
+ function applyConfigHintsToCachedModels(name: string, prov: OcxProviderConfig, models: CatalogModel[], contextCap?: number): CatalogModel[] {
622
+ return models.map(model => applyProviderConfigHints(name, prov, model, contextCap));
607
623
  }
608
624
 
609
625
  function isGlm52ModelId(id: string): boolean {
@@ -641,26 +657,26 @@ function catalogHintsFromModelsApiItem(providerName: string, item: ProviderModel
641
657
  * fetch failure → last-known-good cache (so a provider blip doesn't drop its models), else the
642
658
  * static config list. This is the per-provider half of jawcode's "always latest" resolver.
643
659
  */
644
- async function fetchProviderModels(name: string, prov: OcxProviderConfig, ttlMs: number): Promise<CatalogModel[]> {
660
+ async function fetchProviderModels(name: string, prov: OcxProviderConfig, ttlMs: number, contextCap?: number): Promise<CatalogModel[]> {
645
661
  if (prov.authMode === "forward") return []; // ChatGPT backend has no /models
646
662
  const apiKey = await resolveModelsAuthToken(name, prov);
647
663
  if (prov.authMode === "oauth" && !apiKey) return []; // not logged in → skip
648
664
  const configured: CatalogModel[] = (prov.models ?? []).map(id => ({
649
665
  id,
650
666
  provider: name,
651
- ...catalogHintsFromProviderConfig(name, prov, id),
667
+ ...catalogHintsFromProviderConfig(name, prov, id, contextCap),
652
668
  }));
653
669
  if (prov.liveModels === false) {
654
670
  return configured;
655
671
  }
656
672
  const fresh = getFreshCached(name, ttlMs);
657
- if (fresh) return applyConfigHintsToCachedModels(name, prov, fresh); // dedups Codex's frequent /v1/models polling within the TTL
673
+ if (fresh) return applyConfigHintsToCachedModels(name, prov, fresh, contextCap); // dedups Codex's frequent /v1/models polling within the TTL
658
674
  const { url, headers } = buildModelsRequest(prov, apiKey);
659
675
  try {
660
676
  const res = await fetch(url, { headers, signal: AbortSignal.timeout(8000) });
661
677
  if (!res.ok) {
662
678
  const stale = getStaleCached(name);
663
- return stale ? applyConfigHintsToCachedModels(name, prov, stale) : configured;
679
+ return stale ? applyConfigHintsToCachedModels(name, prov, stale, contextCap) : configured;
664
680
  }
665
681
  const json = await res.json() as { data?: ProviderModelsApiItem[] };
666
682
  const live = (json.data ?? []).map(m => applyProviderConfigHints(name, prov, {
@@ -668,7 +684,7 @@ async function fetchProviderModels(name: string, prov: OcxProviderConfig, ttlMs:
668
684
  provider: name,
669
685
  owned_by: m.owned_by,
670
686
  ...catalogHintsFromModelsApiItem(name, m),
671
- }));
687
+ }, contextCap));
672
688
  const liveIds = new Set(live.map(m => m.id));
673
689
  // Merge explicit config additions (e.g. a model not in the provider's /models, like a new endpoint).
674
690
  const merged = [...live, ...configured.filter(m => !liveIds.has(m.id))];
@@ -676,7 +692,7 @@ async function fetchProviderModels(name: string, prov: OcxProviderConfig, ttlMs:
676
692
  return merged;
677
693
  } catch {
678
694
  const stale = getStaleCached(name);
679
- return stale ? applyConfigHintsToCachedModels(name, prov, stale) : configured;
695
+ return stale ? applyConfigHintsToCachedModels(name, prov, stale, contextCap) : configured;
680
696
  }
681
697
  }
682
698
 
@@ -689,9 +705,9 @@ async function fetchProviderModels(name: string, prov: OcxProviderConfig, ttlMs:
689
705
  export async function gatherRoutedModels(config: OcxConfig): Promise<CatalogModel[]> {
690
706
  const ttlMs = config.modelCacheTtlMs ?? DEFAULT_MODEL_CACHE_TTL_MS;
691
707
  const lists = await Promise.all(
692
- Object.entries(config.providers).map(([name, prov]) => fetchProviderModels(name, prov, ttlMs)),
708
+ Object.entries(config.providers).map(([name, prov]) => fetchProviderModels(name, prov, ttlMs, providerContextCap(config, name))),
693
709
  );
694
- const all = augmentRoutedModelsWithJawcodeMetadata(lists.flat(), Object.keys(config.providers), config.providers)
710
+ const all = augmentRoutedModelsWithJawcodeMetadata(lists.flat(), Object.keys(config.providers), config.providers, config)
695
711
  // Drop image/video generation models (e.g. Grok image/video) — they are not usable by Codex and
696
712
  // must not surface in the dashboard, /v1/models, or the routed catalog. Single choke point.
697
713
  .filter(m => !isMediaGenerationModelId(m.id));
@@ -699,7 +715,12 @@ export async function gatherRoutedModels(config: OcxConfig): Promise<CatalogMode
699
715
  return all;
700
716
  }
701
717
 
702
- export function augmentRoutedModelsWithJawcodeMetadata(models: CatalogModel[], providerNames: string[], providers?: Record<string, OcxProviderConfig>): CatalogModel[] {
718
+ export function augmentRoutedModelsWithJawcodeMetadata(
719
+ models: CatalogModel[],
720
+ providerNames: string[],
721
+ providers?: Record<string, OcxProviderConfig>,
722
+ caps?: Pick<OcxConfig, "providerContextCaps">,
723
+ ): CatalogModel[] {
703
724
  const out = [...models];
704
725
  const seen = new Set(out.map(m => `${m.provider}/${m.id}`));
705
726
  for (const provider of providerNames) {
@@ -711,7 +732,18 @@ export function augmentRoutedModelsWithJawcodeMetadata(models: CatalogModel[], p
711
732
  const key = `${provider}/${meta.id}`;
712
733
  if (seen.has(key)) continue;
713
734
  seen.add(key);
714
- out.push({ provider, id: meta.id, owned_by: provider, ...(providers?.[provider] ? catalogHintsFromProviderConfig(provider, providers[provider], meta.id) : {}) });
735
+ const contextCap = caps ? providerContextCap(caps, provider) : undefined;
736
+ const model: CatalogModel = {
737
+ provider,
738
+ id: meta.id,
739
+ owned_by: provider,
740
+ ...(typeof meta.contextWindow === "number" && meta.contextWindow > 0 ? { contextWindow: meta.contextWindow } : {}),
741
+ ...(Array.isArray(meta.input) && meta.input.length > 0 ? { inputModalities: [...meta.input] } : {}),
742
+ };
743
+ out.push({
744
+ ...model,
745
+ ...(providers?.[provider] ? applyProviderConfigHints(provider, providers[provider], model, contextCap) : {}),
746
+ });
715
747
  }
716
748
  }
717
749
  return out;
package/src/config.ts CHANGED
@@ -98,6 +98,7 @@ const configSchema = z.object({
98
98
  port: z.number().int().min(0).max(65535).default(10100),
99
99
  providers: z.record(z.string(), providerConfigSchema),
100
100
  defaultProvider: z.string().min(1).default("openai"),
101
+ providerContextCaps: z.record(z.string(), z.number().int().positive()).optional(),
101
102
  }).passthrough().superRefine((config, ctx) => {
102
103
  for (const name of Object.keys(config.providers)) {
103
104
  if (!isValidProviderName(name)) {
@@ -0,0 +1,36 @@
1
+ import type { OcxConfig } from "./types";
2
+
3
+ export const DEFAULT_PROVIDER_CONTEXT_CAP = 350_000;
4
+
5
+ function isValidContextCap(value: unknown): value is number {
6
+ return typeof value === "number" && Number.isFinite(value) && value > 0;
7
+ }
8
+
9
+ export function providerContextCap(config: Pick<OcxConfig, "providerContextCaps">, provider: string): number | undefined {
10
+ const value = config.providerContextCaps?.[provider];
11
+ return isValidContextCap(value) ? value : undefined;
12
+ }
13
+
14
+ export function providerContextCaps(config: Pick<OcxConfig, "providerContextCaps">): Record<string, number> {
15
+ const caps = config.providerContextCaps;
16
+ if (!caps || typeof caps !== "object" || Array.isArray(caps)) return {};
17
+ const out: Record<string, number> = {};
18
+ for (const [provider, value] of Object.entries(caps)) {
19
+ if (isValidContextCap(value)) out[provider] = value;
20
+ }
21
+ return out;
22
+ }
23
+
24
+ export function applyProviderContextCap(contextWindow: number | undefined, cap: number | undefined): number | undefined {
25
+ if (!isValidContextCap(cap)) return contextWindow;
26
+ if (!isValidContextCap(contextWindow)) return contextWindow;
27
+ return contextWindow > cap ? cap : contextWindow;
28
+ }
29
+
30
+ export function setProviderContextCap(config: OcxConfig, provider: string, enabled: boolean): void {
31
+ const next = providerContextCaps(config);
32
+ if (enabled) next[provider] = DEFAULT_PROVIDER_CONTEXT_CAP;
33
+ else delete next[provider];
34
+ if (Object.keys(next).length > 0) config.providerContextCaps = next;
35
+ else delete config.providerContextCaps;
36
+ }
package/src/server.ts CHANGED
@@ -48,6 +48,7 @@ import { enrichProviderFromCatalog, listKeyLoginProviders } from "./oauth/key-pr
48
48
  import { deriveProviderPresets } from "./providers/derive";
49
49
  import type { AdapterEvent, OcxConfig, OcxProviderConfig } from "./types";
50
50
  import type { OcxUsage } from "./types";
51
+ import { DEFAULT_PROVIDER_CONTEXT_CAP, providerContextCap, providerContextCaps, setProviderContextCap } from "./provider-context-cap";
51
52
  import {
52
53
  appendUsageEntry,
53
54
  readUsageEntries,
@@ -454,7 +455,7 @@ async function handleResponses(
454
455
  // whose cancel() aborts the upstream — preventing leaked connections (RC2, passthrough path).
455
456
  const upstream = new AbortController();
456
457
  linkAbortSignal(upstream, options.abortSignal);
457
- const connectMs = config.connectTimeoutMs ?? 30_000;
458
+ const connectMs = config.connectTimeoutMs ?? 100_000;
458
459
  let upstreamResponse: Response;
459
460
  try {
460
461
  upstreamResponse = await fetchWithHeaderTimeout(request.url, {
@@ -586,7 +587,7 @@ async function handleResponses(
586
587
 
587
588
  const upstream = new AbortController();
588
589
  const cleanupUpstreamAbort = linkAbortSignal(upstream, options.abortSignal);
589
- const connectMs = config.connectTimeoutMs ?? 30_000;
590
+ const connectMs = config.connectTimeoutMs ?? 100_000;
590
591
 
591
592
  const request = adapter.buildRequest(parsed, { headers: selectedForwardHeaders });
592
593
  if (typeof request.usageLog?.inputTokens === "number") {
@@ -1759,6 +1760,7 @@ async function handleManagementAPI(req: Request, url: URL, config: OcxConfig): P
1759
1760
  if (name === config.defaultProvider) return jsonResponse({ error: "cannot delete the default provider; set another default first" }, 400);
1760
1761
  const { saveConfig: save } = await import("./config");
1761
1762
  delete config.providers[name];
1763
+ setProviderContextCap(config, name, false);
1762
1764
  save(config);
1763
1765
  const { clearModelCache: clearCache } = await import("./model-cache");
1764
1766
  clearCache(name);
@@ -1771,10 +1773,42 @@ async function handleManagementAPI(req: Request, url: URL, config: OcxConfig): P
1771
1773
  const disabled = new Set(config.disabledModels ?? []);
1772
1774
  return jsonResponse(models.map(m => {
1773
1775
  const namespaced = `${m.provider}/${m.id}`;
1774
- return { ...m, namespaced, disabled: disabled.has(namespaced) };
1776
+ const contextCap = providerContextCap(config, m.provider);
1777
+ return {
1778
+ ...m,
1779
+ namespaced,
1780
+ disabled: disabled.has(namespaced),
1781
+ ...(contextCap !== undefined ? { contextCap, contextCapped: m.contextCapped === true } : {}),
1782
+ };
1775
1783
  }));
1776
1784
  }
1777
1785
 
1786
+ if (url.pathname === "/api/provider-context-caps" && req.method === "GET") {
1787
+ return jsonResponse({ cap: DEFAULT_PROVIDER_CONTEXT_CAP, caps: providerContextCaps(config) });
1788
+ }
1789
+
1790
+ if (url.pathname === "/api/provider-context-caps" && req.method === "PUT") {
1791
+ let body: { provider?: unknown; enabled?: unknown };
1792
+ try { body = await req.json(); } catch { return jsonResponse({ error: "invalid JSON body" }, 400); }
1793
+ if (typeof body.provider !== "string" || typeof body.enabled !== "boolean") {
1794
+ return jsonResponse({ error: "provider string and enabled boolean are required" }, 400);
1795
+ }
1796
+ const provider = body.provider.trim();
1797
+ if (!isValidProviderName(provider)) {
1798
+ return jsonResponse({ error: "provider name must use letters, numbers, dot, underscore, or hyphen and cannot be a reserved object key" }, 400);
1799
+ }
1800
+ if (!hasOwnProvider(config.providers, provider)) {
1801
+ return jsonResponse({ error: "unknown provider" }, 404);
1802
+ }
1803
+ setProviderContextCap(config, provider, body.enabled);
1804
+ const { saveConfig: save } = await import("./config");
1805
+ save(config);
1806
+ const { clearModelCache } = await import("./model-cache");
1807
+ clearModelCache(provider);
1808
+ await refreshCodexCatalogBestEffort();
1809
+ return jsonResponse({ ok: true, cap: DEFAULT_PROVIDER_CONTEXT_CAP, caps: providerContextCaps(config) });
1810
+ }
1811
+
1778
1812
  // Enable/disable models: which routed models Codex sees. PUT hides them from the catalog +
1779
1813
  // /v1/models and invalidates Codex's 5-min models cache so it applies on the next turn.
1780
1814
  if (url.pathname === "/api/disabled-models" && req.method === "PUT") {
package/src/types.ts CHANGED
@@ -205,6 +205,8 @@ export interface OcxConfig {
205
205
  subagentModels?: string[];
206
206
  /** Routed model ids ("<provider>/<model>") hidden from Codex (excluded from the catalog + /v1/models). */
207
207
  disabledModels?: string[];
208
+ /** Provider-level Codex-visible context caps. Values only lower known model context windows. */
209
+ providerContextCaps?: Record<string, number>;
208
210
  /** Bind hostname. Default "127.0.0.1" (loopback only). Set "0.0.0.0" to expose on all interfaces. */
209
211
  hostname?: string;
210
212
  /** Upstream stall timeout (seconds). After this many seconds of no upstream data, emits response.incomplete. Default 90. Min 1. */