@bitkyc08/opencodex 2.1.5 → 2.1.7

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.
@@ -129,6 +129,9 @@ function cloneProviderField(value: unknown): unknown {
129
129
 
130
130
  const OAUTH_RECONCILE_FIELDS: (keyof OcxProviderConfig)[] = [
131
131
  "models",
132
+ "contextWindow",
133
+ "modelContextWindows",
134
+ "modelInputModalities",
132
135
  "noReasoningModels",
133
136
  "noVisionModels",
134
137
  "reasoningEfforts",
@@ -15,6 +15,9 @@ export interface KeyLoginProvider {
15
15
  dashboardUrl: string;
16
16
  models?: string[];
17
17
  defaultModel?: string;
18
+ contextWindow?: number;
19
+ modelContextWindows?: Record<string, number>;
20
+ modelInputModalities?: Record<string, string[]>;
18
21
  /**
19
22
  * Model ids that do NOT accept image input (the vision sidecar describes images for them) / do NOT
20
23
  * accept a reasoning param. Copied into the created provider config by `enrichProviderFromCatalog`,
@@ -31,6 +34,7 @@ export interface KeyLoginProvider {
31
34
  noPenaltyModels?: string[];
32
35
  autoToolChoiceOnlyModels?: string[];
33
36
  preserveReasoningContentModels?: string[];
37
+ escapeBuiltinToolNames?: boolean;
34
38
  }
35
39
 
36
40
  export const KEY_LOGIN_PROVIDERS: Record<string, KeyLoginProvider> = deriveKeyLoginMap();
@@ -46,6 +50,9 @@ export function enrichProviderFromCatalog(name: string, prov: OcxProviderConfig)
46
50
  if (!e) return;
47
51
  if (!prov.models && e.models) prov.models = [...e.models];
48
52
  if (!prov.defaultModel && e.defaultModel) prov.defaultModel = e.defaultModel;
53
+ if (prov.contextWindow === undefined && e.contextWindow !== undefined) prov.contextWindow = e.contextWindow;
54
+ if (!prov.modelContextWindows && e.modelContextWindows) prov.modelContextWindows = { ...e.modelContextWindows };
55
+ if (!prov.modelInputModalities && e.modelInputModalities) prov.modelInputModalities = cloneRecordOfArrays(e.modelInputModalities);
49
56
  if (!prov.reasoningEfforts && e.reasoningEfforts) prov.reasoningEfforts = [...e.reasoningEfforts];
50
57
  if (!prov.modelReasoningEfforts && e.modelReasoningEfforts) prov.modelReasoningEfforts = cloneRecordOfArrays(e.modelReasoningEfforts);
51
58
  if (!prov.reasoningEffortMap && e.reasoningEffortMap) prov.reasoningEffortMap = { ...e.reasoningEffortMap };
@@ -57,6 +64,7 @@ export function enrichProviderFromCatalog(name: string, prov: OcxProviderConfig)
57
64
  if (!prov.noPenaltyModels && e.noPenaltyModels) prov.noPenaltyModels = [...e.noPenaltyModels];
58
65
  if (!prov.autoToolChoiceOnlyModels && e.autoToolChoiceOnlyModels) prov.autoToolChoiceOnlyModels = [...e.autoToolChoiceOnlyModels];
59
66
  if (!prov.preserveReasoningContentModels && e.preserveReasoningContentModels) prov.preserveReasoningContentModels = [...e.preserveReasoningContentModels];
67
+ if (prov.escapeBuiltinToolNames === undefined && e.escapeBuiltinToolNames !== undefined) prov.escapeBuiltinToolNames = e.escapeBuiltinToolNames;
60
68
  }
61
69
 
62
70
 
@@ -76,10 +84,31 @@ export function listKeyLoginProviders(): Array<{ id: string } & KeyLoginProvider
76
84
  return Object.entries(KEY_LOGIN_PROVIDERS).map(([id, p]) => ({ id, ...p }));
77
85
  }
78
86
 
79
- /** Best-effort key validation: GET {baseUrl}/models with the key. Returns true/false/unknown. */
80
- export async function validateApiKey(baseUrl: string, key: string): Promise<boolean | "unknown"> {
87
+ /** Best-effort key validation. Returns true/false/unknown; never persists the key itself. */
88
+ export async function validateApiKey(provider: KeyLoginProvider, key: string): Promise<boolean | "unknown"> {
81
89
  try {
82
- const res = await fetch(`${baseUrl}/models`, {
90
+ if (provider.adapter === "anthropic") {
91
+ const base = provider.baseUrl.replace(/\/v1\/?$/, "");
92
+ const res = await fetch(`${base}/v1/messages`, {
93
+ method: "POST",
94
+ headers: {
95
+ "Content-Type": "application/json",
96
+ "anthropic-version": "2023-06-01",
97
+ "x-api-key": key,
98
+ },
99
+ body: JSON.stringify({
100
+ model: provider.defaultModel ?? "claude-sonnet-4-6",
101
+ max_tokens: 1,
102
+ messages: [{ role: "user", content: "ping" }],
103
+ }),
104
+ signal: AbortSignal.timeout(8000),
105
+ });
106
+ if (res.ok) return true;
107
+ if (res.status === 401 || res.status === 403) return false;
108
+ return "unknown";
109
+ }
110
+
111
+ const res = await fetch(`${provider.baseUrl}/models`, {
83
112
  headers: { Authorization: `Bearer ${key}` },
84
113
  signal: AbortSignal.timeout(8000),
85
114
  });
@@ -2,7 +2,8 @@ import * as readline from "node:readline";
2
2
  import { openUrl } from "../open-url";
3
3
  import { loadConfig, readPid, saveConfig } from "../config";
4
4
  import { OAUTH_PROVIDERS, runLogin } from "./index";
5
- import { KEY_LOGIN_PROVIDERS, isKeyLoginProvider, validateApiKey } from "./key-providers";
5
+ import { KEY_LOGIN_PROVIDERS, isKeyLoginProvider, validateApiKey, type KeyLoginProvider } from "./key-providers";
6
+ import type { OcxProviderConfig } from "../types";
6
7
 
7
8
  /** Push the new provider into a running proxy's live config so it routes without a restart. */
8
9
  async function notifyRunningProxy(name: string, provider: unknown): Promise<void> {
@@ -51,6 +52,31 @@ async function handleOAuthLogin(name: string): Promise<void> {
51
52
  console.log(`\n✅ Logged in to ${name}. Try: ocx sync`);
52
53
  }
53
54
 
55
+ export function providerConfigFromKeyLoginProvider(def: KeyLoginProvider, key: string): OcxProviderConfig {
56
+ return {
57
+ adapter: def.adapter,
58
+ baseUrl: def.baseUrl,
59
+ apiKey: key,
60
+ ...(def.defaultModel ? { defaultModel: def.defaultModel } : {}),
61
+ ...(def.models ? { models: [...def.models] } : {}),
62
+ ...(def.contextWindow !== undefined ? { contextWindow: def.contextWindow } : {}),
63
+ ...(def.modelContextWindows ? { modelContextWindows: { ...def.modelContextWindows } } : {}),
64
+ ...(def.modelInputModalities ? { modelInputModalities: cloneRecordOfArrays(def.modelInputModalities) } : {}),
65
+ ...(def.reasoningEfforts ? { reasoningEfforts: [...def.reasoningEfforts] } : {}),
66
+ ...(def.modelReasoningEfforts ? { modelReasoningEfforts: cloneRecordOfArrays(def.modelReasoningEfforts) } : {}),
67
+ ...(def.reasoningEffortMap ? { reasoningEffortMap: { ...def.reasoningEffortMap } } : {}),
68
+ ...(def.modelReasoningEffortMap ? { modelReasoningEffortMap: cloneNestedRecord(def.modelReasoningEffortMap) } : {}),
69
+ ...(def.noVisionModels ? { noVisionModels: [...def.noVisionModels] } : {}),
70
+ ...(def.noReasoningModels ? { noReasoningModels: [...def.noReasoningModels] } : {}),
71
+ ...(def.noTemperatureModels ? { noTemperatureModels: [...def.noTemperatureModels] } : {}),
72
+ ...(def.noTopPModels ? { noTopPModels: [...def.noTopPModels] } : {}),
73
+ ...(def.noPenaltyModels ? { noPenaltyModels: [...def.noPenaltyModels] } : {}),
74
+ ...(def.autoToolChoiceOnlyModels ? { autoToolChoiceOnlyModels: [...def.autoToolChoiceOnlyModels] } : {}),
75
+ ...(def.preserveReasoningContentModels ? { preserveReasoningContentModels: [...def.preserveReasoningContentModels] } : {}),
76
+ ...(def.escapeBuiltinToolNames !== undefined ? { escapeBuiltinToolNames: def.escapeBuiltinToolNames } : {}),
77
+ };
78
+ }
79
+
54
80
  async function handleKeyLogin(name: string): Promise<void> {
55
81
  const def = KEY_LOGIN_PROVIDERS[name];
56
82
  console.log(`\n🔑 ${def.label} — opening ${def.dashboardUrl} so you can create/copy an API key...`);
@@ -63,22 +89,24 @@ async function handleKeyLogin(name: string): Promise<void> {
63
89
  process.exit(1);
64
90
  }
65
91
  process.stdout.write(" validating… ");
66
- const valid = await validateApiKey(def.baseUrl, key);
92
+ const valid = await validateApiKey(def, key);
67
93
  console.log(valid === true ? "valid ✅" : valid === false ? "INVALID ❌" : "couldn't validate (may still work)");
68
94
  if (valid === false) {
69
95
  console.error("Provider rejected the key. Not saved.");
70
96
  process.exit(1);
71
97
  }
72
- const provider = {
73
- adapter: def.adapter,
74
- baseUrl: def.baseUrl,
75
- apiKey: key,
76
- ...(def.defaultModel ? { defaultModel: def.defaultModel } : {}),
77
- ...(def.models ? { models: def.models } : {}),
78
- };
98
+ const provider = providerConfigFromKeyLoginProvider(def, key);
79
99
  const config = loadConfig();
80
100
  config.providers[name] = provider;
81
101
  saveConfig(config);
82
102
  await notifyRunningProxy(name, provider);
83
103
  console.log(`✅ ${def.label} added. Try: ocx sync`);
84
104
  }
105
+
106
+ function cloneRecordOfArrays(input: Record<string, string[]>): Record<string, string[]> {
107
+ return Object.fromEntries(Object.entries(input).map(([key, value]) => [key, [...value]]));
108
+ }
109
+
110
+ function cloneNestedRecord(input: Record<string, Record<string, string>>): Record<string, Record<string, string>> {
111
+ return Object.fromEntries(Object.entries(input).map(([key, value]) => [key, { ...value }]));
112
+ }
@@ -8,6 +8,9 @@ export interface DerivedKeyLoginProvider {
8
8
  dashboardUrl: string;
9
9
  models?: string[];
10
10
  defaultModel?: string;
11
+ contextWindow?: number;
12
+ modelContextWindows?: Record<string, number>;
13
+ modelInputModalities?: Record<string, string[]>;
11
14
  reasoningEfforts?: string[];
12
15
  modelReasoningEfforts?: Record<string, string[]>;
13
16
  reasoningEffortMap?: Record<string, string>;
@@ -19,6 +22,7 @@ export interface DerivedKeyLoginProvider {
19
22
  noPenaltyModels?: string[];
20
23
  autoToolChoiceOnlyModels?: string[];
21
24
  preserveReasoningContentModels?: string[];
25
+ escapeBuiltinToolNames?: boolean;
22
26
  }
23
27
 
24
28
  export interface DerivedInitProvider {
@@ -62,6 +66,9 @@ export function providerConfigSeed(entry: ProviderRegistryEntry): OcxProviderCon
62
66
  authMode: entry.authKind === "local" ? undefined : entry.authKind,
63
67
  ...(entry.defaultModel ? { defaultModel: entry.defaultModel } : {}),
64
68
  ...(entry.models ? { models: [...entry.models] } : {}),
69
+ ...(entry.contextWindow !== undefined ? { contextWindow: entry.contextWindow } : {}),
70
+ ...(entry.modelContextWindows ? { modelContextWindows: { ...entry.modelContextWindows } } : {}),
71
+ ...(entry.modelInputModalities ? { modelInputModalities: cloneRecordOfArrays(entry.modelInputModalities) } : {}),
65
72
  ...(entry.reasoningEfforts ? { reasoningEfforts: [...entry.reasoningEfforts] } : {}),
66
73
  ...(entry.modelReasoningEfforts ? { modelReasoningEfforts: cloneRecordOfArrays(entry.modelReasoningEfforts) } : {}),
67
74
  ...(entry.reasoningEffortMap ? { reasoningEffortMap: { ...entry.reasoningEffortMap } } : {}),
@@ -73,6 +80,7 @@ export function providerConfigSeed(entry: ProviderRegistryEntry): OcxProviderCon
73
80
  ...(entry.noPenaltyModels ? { noPenaltyModels: [...entry.noPenaltyModels] } : {}),
74
81
  ...(entry.autoToolChoiceOnlyModels ? { autoToolChoiceOnlyModels: [...entry.autoToolChoiceOnlyModels] } : {}),
75
82
  ...(entry.preserveReasoningContentModels ? { preserveReasoningContentModels: [...entry.preserveReasoningContentModels] } : {}),
83
+ ...(entry.escapeBuiltinToolNames !== undefined ? { escapeBuiltinToolNames: entry.escapeBuiltinToolNames } : {}),
76
84
  };
77
85
  }
78
86
 
@@ -88,6 +96,9 @@ export function deriveKeyLoginMap(): Record<string, DerivedKeyLoginProvider> {
88
96
  dashboardUrl: entry.dashboardUrl,
89
97
  ...(entry.models ? { models: [...entry.models] } : {}),
90
98
  ...(entry.defaultModel ? { defaultModel: entry.defaultModel } : {}),
99
+ ...(entry.contextWindow !== undefined ? { contextWindow: entry.contextWindow } : {}),
100
+ ...(entry.modelContextWindows ? { modelContextWindows: { ...entry.modelContextWindows } } : {}),
101
+ ...(entry.modelInputModalities ? { modelInputModalities: cloneRecordOfArrays(entry.modelInputModalities) } : {}),
91
102
  ...(entry.reasoningEfforts ? { reasoningEfforts: [...entry.reasoningEfforts] } : {}),
92
103
  ...(entry.modelReasoningEfforts ? { modelReasoningEfforts: cloneRecordOfArrays(entry.modelReasoningEfforts) } : {}),
93
104
  ...(entry.reasoningEffortMap ? { reasoningEffortMap: { ...entry.reasoningEffortMap } } : {}),
@@ -99,6 +110,7 @@ export function deriveKeyLoginMap(): Record<string, DerivedKeyLoginProvider> {
99
110
  ...(entry.noPenaltyModels ? { noPenaltyModels: [...entry.noPenaltyModels] } : {}),
100
111
  ...(entry.autoToolChoiceOnlyModels ? { autoToolChoiceOnlyModels: [...entry.autoToolChoiceOnlyModels] } : {}),
101
112
  ...(entry.preserveReasoningContentModels ? { preserveReasoningContentModels: [...entry.preserveReasoningContentModels] } : {}),
113
+ ...(entry.escapeBuiltinToolNames !== undefined ? { escapeBuiltinToolNames: entry.escapeBuiltinToolNames } : {}),
102
114
  };
103
115
  }
104
116
  return out;
@@ -14,6 +14,9 @@ export interface ProviderRegistryEntry {
14
14
  dashboardUrl?: string;
15
15
  defaultModel?: string;
16
16
  models?: string[];
17
+ contextWindow?: number;
18
+ modelContextWindows?: Record<string, number>;
19
+ modelInputModalities?: Record<string, string[]>;
17
20
  reasoningEfforts?: string[];
18
21
  modelReasoningEfforts?: Record<string, string[]>;
19
22
  reasoningEffortMap?: Record<string, string>;
@@ -25,6 +28,7 @@ export interface ProviderRegistryEntry {
25
28
  noPenaltyModels?: string[];
26
29
  autoToolChoiceOnlyModels?: string[];
27
30
  preserveReasoningContentModels?: string[];
31
+ escapeBuiltinToolNames?: boolean;
28
32
  oauthId?: string;
29
33
  jawcodeBundle?: string;
30
34
  extraMetadataAliases?: string[];
@@ -34,9 +38,10 @@ export interface ProviderRegistryEntry {
34
38
  export type ProviderConfigSeed = Pick<
35
39
  OcxProviderConfig,
36
40
  "adapter" | "baseUrl" | "authMode" | "defaultModel" | "models"
41
+ | "contextWindow" | "modelContextWindows" | "modelInputModalities"
37
42
  | "reasoningEfforts" | "modelReasoningEfforts" | "reasoningEffortMap" | "modelReasoningEffortMap"
38
43
  | "noVisionModels" | "noReasoningModels" | "noTemperatureModels" | "noTopPModels" | "noPenaltyModels"
39
- | "autoToolChoiceOnlyModels" | "preserveReasoningContentModels"
44
+ | "autoToolChoiceOnlyModels" | "preserveReasoningContentModels" | "escapeBuiltinToolNames"
40
45
  >;
41
46
 
42
47
 
@@ -58,6 +63,39 @@ const NEURALWATT_REASONING_HISTORY_MODELS = [
58
63
  "moonshotai/Kimi-K2.5", "kimi-k2.6", "kimi-k2.7-code",
59
64
  "qwen3.5-397b", "qwen3.6-35b",
60
65
  ];
66
+ const UMANS_MODELS = [
67
+ "umans-coder",
68
+ "umans-kimi-k2.7",
69
+ "umans-kimi-k2.6",
70
+ "umans-flash",
71
+ "umans-glm-5.2",
72
+ "umans-glm-5.1",
73
+ "umans-qwen3.6-35b-a3b",
74
+ ];
75
+ const UMANS_REASONING_EFFORTS = ["low", "medium", "high", "xhigh"];
76
+ const UMANS_GLM_REASONING_EFFORTS = ["high", "xhigh"];
77
+ const UMANS_GLM_REASONING_MAP: Record<string, string> = {
78
+ none: "high",
79
+ minimal: "high",
80
+ low: "high",
81
+ medium: "high",
82
+ high: "high",
83
+ xhigh: "max",
84
+ max: "max",
85
+ };
86
+ const UMANS_TEXT_ONLY_MODELS = ["umans-glm-5.2", "umans-glm-5.1"];
87
+ const UMANS_MODEL_CONTEXT_WINDOWS: Record<string, number> = {
88
+ "umans-coder": 262_144,
89
+ "umans-kimi-k2.7": 262_144,
90
+ "umans-kimi-k2.6": 262_144,
91
+ "umans-flash": 262_144,
92
+ "umans-glm-5.2": 405_504,
93
+ "umans-glm-5.1": 202_752,
94
+ "umans-qwen3.6-35b-a3b": 262_144,
95
+ };
96
+ const UMANS_MODEL_INPUT_MODALITIES: Record<string, string[]> = Object.fromEntries(
97
+ UMANS_MODELS.map(id => [id, UMANS_TEXT_ONLY_MODELS.includes(id) ? ["text"] : ["text", "image"]]),
98
+ );
61
99
 
62
100
  export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
63
101
  {
@@ -119,6 +157,35 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
119
157
  preserveReasoningContentModels: KIMI_THINKING_MODELS,
120
158
  },
121
159
  { id: "openai-apikey", label: "OpenAI (API key)", adapter: "openai-responses", baseUrl: "https://api.openai.com/v1", authKind: "key", featured: true, dashboardUrl: "https://platform.openai.com/api-keys", defaultModel: "gpt-5.5" },
160
+ {
161
+ id: "umans",
162
+ label: "Umans AI Coding Plan",
163
+ adapter: "anthropic",
164
+ baseUrl: "https://api.code.umans.ai",
165
+ authKind: "key",
166
+ featured: true,
167
+ dashboardUrl: "https://app.umans.ai/billing",
168
+ defaultModel: "umans-coder",
169
+ models: UMANS_MODELS,
170
+ modelContextWindows: UMANS_MODEL_CONTEXT_WINDOWS,
171
+ modelInputModalities: UMANS_MODEL_INPUT_MODALITIES,
172
+ note: "Coding plan via Anthropic Messages",
173
+ modelReasoningEfforts: {
174
+ "umans-coder": UMANS_REASONING_EFFORTS,
175
+ "umans-kimi-k2.7": UMANS_REASONING_EFFORTS,
176
+ "umans-kimi-k2.6": UMANS_REASONING_EFFORTS,
177
+ "umans-flash": ["low", "medium", "high"],
178
+ "umans-glm-5.2": UMANS_GLM_REASONING_EFFORTS,
179
+ "umans-glm-5.1": UMANS_GLM_REASONING_EFFORTS,
180
+ "umans-qwen3.6-35b-a3b": ["low", "medium", "high"],
181
+ },
182
+ modelReasoningEffortMap: {
183
+ "umans-glm-5.2": UMANS_GLM_REASONING_MAP,
184
+ "umans-glm-5.1": UMANS_GLM_REASONING_MAP,
185
+ },
186
+ noVisionModels: UMANS_TEXT_ONLY_MODELS,
187
+ escapeBuiltinToolNames: true,
188
+ },
122
189
  {
123
190
  id: "opencode-go", label: "opencode go", adapter: "openai-chat", baseUrl: "https://opencode.ai/zen/go/v1",
124
191
  authKind: "key", featured: true, dashboardUrl: "https://opencode.ai/auth", defaultModel: "kimi-k2.7-code",
package/src/types.ts CHANGED
@@ -187,6 +187,13 @@ export interface OcxConfig {
187
187
  websockets?: boolean;
188
188
  /** Auto-start/sync the proxy from the Codex shim before launching Codex. Default true. */
189
189
  codexAutoStart?: boolean;
190
+ /**
191
+ * Compatibility mode: temporarily rewrite Codex resume-history metadata while the proxy is active
192
+ * so Codex App can show old OpenAI chats and opencodex-created exec chats under its default
193
+ * interactive-source/provider filters. Disabled by default because it mutates Codex's local
194
+ * thread index; originals are backed up and restored by `ocx stop` / `ocx restore`.
195
+ */
196
+ syncResumeHistory?: boolean;
190
197
  /** Freshness window (ms) for the per-provider live `/models` cache. Defaults to 5 min. */
191
198
  modelCacheTtlMs?: number;
192
199
  /** Web-search sidecar: route web_search for non-OpenAI models through a gpt-mini via ChatGPT passthrough. */
@@ -223,6 +230,12 @@ export interface OcxProviderConfig {
223
230
  apiKey?: string;
224
231
  defaultModel?: string;
225
232
  models?: string[];
233
+ /** Provider-wide Codex-visible context-window cap for routed catalog entries. */
234
+ contextWindow?: number;
235
+ /** Model-specific Codex-visible context-window caps. Values cap live metadata, never raise it. */
236
+ modelContextWindows?: Record<string, number>;
237
+ /** Model-specific Codex catalog input modalities, e.g. ["text"] or ["text", "image"]. */
238
+ modelInputModalities?: Record<string, string[]>;
226
239
  headers?: Record<string, string>;
227
240
  /**
228
241
  * "key" (default): authenticate upstream with `apiKey`.
@@ -258,6 +271,8 @@ export interface OcxProviderConfig {
258
271
  autoToolChoiceOnlyModels?: string[];
259
272
  /** Model ids that expect prior assistant `reasoning_content` to be preserved in chat history. */
260
273
  preserveReasoningContentModels?: string[];
274
+ /** Anthropic-compatible gateways that need custom tool names escaped on the wire. */
275
+ escapeBuiltinToolNames?: boolean;
261
276
  /**
262
277
  * Model ids that do NOT accept image inputs. The proxy gives them "eyes" via the vision sidecar:
263
278
  * attached images are described by a gpt vision model and replaced with text before the call.