@bitkyc08/opencodex 1.9.4 → 2.0.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.
@@ -1,11 +1,20 @@
1
1
  import { existsSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
2
2
  import { atomicWriteFile, websocketsEnabled } from "./config";
3
3
  import { restoreCodexCatalog } from "./codex-catalog";
4
- import { CODEX_CONFIG_PATH, CODEX_PROFILE_PATH, DEFAULT_CATALOG_PATH, parseTomlString, readRootTomlString, tomlString } from "./codex-paths";
4
+ import { CODEX_CONFIG_PATH, CODEX_PROFILE_PATH, DEFAULT_CATALOG_PATH, parseTomlString, readRootTomlString, resolveCodexConfigPath, tomlString } from "./codex-paths";
5
5
  import type { OcxConfig } from "./types";
6
6
 
7
7
  const OCX_SECTION_MARKER = "# Auto-injected by opencodex";
8
8
 
9
+ export interface InjectCodexOptions {
10
+ /**
11
+ * Absolute or CODEX_HOME-relative catalog path to advertise to Codex. Pass `null` only when the
12
+ * opencodex catalog could not be materialized; Codex will then keep its native catalog instead of
13
+ * failing on a missing model_catalog_json file.
14
+ */
15
+ catalogPath?: string | null;
16
+ }
17
+
9
18
  /**
10
19
  * The `[model_providers.opencodex]` TABLE only. A table is position-independent in TOML, so it is
11
20
  * safe to append at EOF. The bare root key `model_provider = "opencodex"` is NOT included here —
@@ -83,10 +92,20 @@ function readRootModelCatalogPath(content: string): string | null {
83
92
  }
84
93
 
85
94
  function setRootModelCatalogPath(content: string, catalogPath: string): string {
86
- if (readRootModelCatalogPath(content)) return content;
87
95
  const lines = content.split("\n");
88
96
  const firstTable = lines.findIndex(l => /^\s*\[/.test(l));
89
97
  const key = `model_catalog_json = ${tomlString(catalogPath)}`;
98
+ const rootEnd = firstTable === -1 ? lines.length : firstTable;
99
+ for (let i = 0; i < rootEnd; i++) {
100
+ const m = lines[i].match(/^\s*model_catalog_json\s*=\s*("(?:\\.|[^"])*"|'[^']*')\s*$/);
101
+ if (!m) continue;
102
+ const existing = parseTomlString(m[1]);
103
+ if (isOpencodexCatalogPath(existing)) {
104
+ lines[i] = key;
105
+ return lines.join("\n");
106
+ }
107
+ return content;
108
+ }
90
109
  if (firstTable === -1) {
91
110
  return content.replace(/\n+$/, "") + "\n" + key + "\n";
92
111
  }
@@ -157,20 +176,30 @@ function stripOpencodexCatalogPath(content: string): string {
157
176
  .join("\n");
158
177
  }
159
178
 
160
- function buildProfileFile(port: number, catalogPath: string): string {
161
- return [
179
+ export function buildProfileFile(port: number, catalogPath?: string | null): string {
180
+ const lines = [
162
181
  "# OpenCodex proxy profile — use with: codex --profile opencodex",
163
182
  `# Routes all model requests through the opencodex proxy at localhost:${port}`,
164
183
  'model_provider = "opencodex"',
165
- `model_catalog_json = ${tomlString(catalogPath)}`,
166
- "",
167
- "[features]",
168
- "fast_mode = true",
169
- "",
170
- ].join("\n");
184
+ ];
185
+ if (catalogPath) lines.push(`model_catalog_json = ${tomlString(catalogPath)}`);
186
+ lines.push("", "[features]", "fast_mode = true", "");
187
+ return lines.join("\n");
188
+ }
189
+
190
+ export function chooseCatalogPathForInjection(content: string, requested?: string | null): string | null {
191
+ if (requested !== undefined) return requested;
192
+
193
+ const existing = readRootModelCatalogPath(content);
194
+ if (existing) {
195
+ const resolved = resolveCodexConfigPath(existing);
196
+ if (!isOpencodexCatalogPath(resolved) || existsSync(resolved)) return existing;
197
+ }
198
+
199
+ return existsSync(DEFAULT_CATALOG_PATH) ? DEFAULT_CATALOG_PATH : null;
171
200
  }
172
201
 
173
- export async function injectCodexConfig(port: number, config?: OcxConfig): Promise<{ success: boolean; message: string }> {
202
+ export async function injectCodexConfig(port: number, config?: OcxConfig, options: InjectCodexOptions = {}): Promise<{ success: boolean; message: string }> {
174
203
  if (!existsSync(CODEX_CONFIG_PATH)) {
175
204
  return { success: false, message: `Codex config not found at ${CODEX_CONFIG_PATH}. Is Codex installed?` };
176
205
  }
@@ -189,8 +218,8 @@ export async function injectCodexConfig(port: number, config?: OcxConfig): Promi
189
218
  content = normalizeServiceTier(content);
190
219
  content = ensureFastModeFeature(content);
191
220
 
192
- const catalogPath = readRootModelCatalogPath(content) ?? DEFAULT_CATALOG_PATH;
193
- content = setRootModelCatalogPath(content, catalogPath);
221
+ const catalogPath = chooseCatalogPathForInjection(content, options.catalogPath);
222
+ content = catalogPath ? setRootModelCatalogPath(content, catalogPath) : stripOpencodexCatalogPath(content);
194
223
 
195
224
  // 1) Root key BEFORE the first table header (must be a global, not nested under a table).
196
225
  content = setRootModelProvider(content);
@@ -200,9 +229,13 @@ export async function injectCodexConfig(port: number, config?: OcxConfig): Promi
200
229
  writeFileSync(CODEX_CONFIG_PATH, content, "utf-8");
201
230
  writeFileSync(CODEX_PROFILE_PATH, buildProfileFile(port, catalogPath), "utf-8");
202
231
 
232
+ const catalogMessage = catalogPath
233
+ ? ` Codex model catalog: ${catalogPath}\n`
234
+ : ` Codex model catalog not injected because no opencodex catalog file exists yet.\n`;
203
235
  return {
204
236
  success: true,
205
237
  message: `Injected opencodex as default provider into Codex config.\n` +
238
+ catalogMessage +
206
239
  ` All models now route through opencodex proxy (like OpenRouter).\n` +
207
240
  ` OpenAI models (gpt-5.5, etc.) are passed through to OpenAI.\n` +
208
241
  ` Custom models route to their configured providers.\n` +
package/src/config.ts CHANGED
@@ -3,12 +3,13 @@ import { homedir } from "node:os";
3
3
  import { join } from "node:path";
4
4
  import type { OcxConfig } from "./types";
5
5
 
6
+ let _atomicSeq = 0;
6
7
  /**
7
8
  * Write a file atomically (temp + rename) so concurrent writers — e.g. `ocx stop` and the
8
9
  * proxy's own shutdown handler both restoring Codex — can never leave a half-written file.
9
10
  */
10
11
  export function atomicWriteFile(path: string, content: string): void {
11
- const tmp = `${path}.ocx.tmp`;
12
+ const tmp = `${path}.ocx.${process.pid}.${++_atomicSeq}.tmp`;
12
13
  writeFileSync(tmp, content, "utf-8");
13
14
  renameSync(tmp, path);
14
15
  }
@@ -121,24 +121,40 @@ export function buildModelsRequest(prov: OcxProviderConfig, apiKey: string | und
121
121
  * Only touches providers that are registry-managed AND still `authMode: "oauth"`, and only the
122
122
  * preset fields (never apiKey/baseUrl/user toggles). Persists + returns true when anything changed.
123
123
  */
124
+ function cloneProviderField(value: unknown): unknown {
125
+ if (Array.isArray(value)) return [...value];
126
+ if (value && typeof value === "object") return JSON.parse(JSON.stringify(value));
127
+ return value;
128
+ }
129
+
130
+ const OAUTH_RECONCILE_FIELDS: (keyof OcxProviderConfig)[] = [
131
+ "models",
132
+ "noReasoningModels",
133
+ "noVisionModels",
134
+ "reasoningEfforts",
135
+ "modelReasoningEfforts",
136
+ "reasoningEffortMap",
137
+ "modelReasoningEffortMap",
138
+ "noTemperatureModels",
139
+ "noTopPModels",
140
+ "noPenaltyModels",
141
+ "autoToolChoiceOnlyModels",
142
+ "preserveReasoningContentModels",
143
+ ];
144
+
124
145
  export function reconcileOAuthProviders(config: OcxConfig): boolean {
125
146
  let changed = false;
126
147
  for (const [name, prov] of Object.entries(config.providers)) {
127
148
  const def = OAUTH_PROVIDERS[name];
128
149
  if (!def || prov.authMode !== "oauth") continue;
129
150
  const preset = def.providerConfig;
130
- if (preset.models && JSON.stringify(prov.models) !== JSON.stringify(preset.models)) {
131
- prov.models = [...preset.models];
132
- changed = true;
133
- }
134
- if (JSON.stringify(prov.noReasoningModels) !== JSON.stringify(preset.noReasoningModels)) {
135
- if (preset.noReasoningModels) prov.noReasoningModels = [...preset.noReasoningModels];
136
- else delete prov.noReasoningModels;
137
- changed = true;
138
- }
139
- if (JSON.stringify(prov.noVisionModels) !== JSON.stringify(preset.noVisionModels)) {
140
- if (preset.noVisionModels) prov.noVisionModels = [...preset.noVisionModels];
141
- else delete prov.noVisionModels;
151
+ for (const field of OAUTH_RECONCILE_FIELDS) {
152
+ if (JSON.stringify(prov[field]) === JSON.stringify(preset[field])) continue;
153
+ if (preset[field] !== undefined) {
154
+ prov[field] = cloneProviderField(preset[field]) as never;
155
+ } else {
156
+ delete prov[field];
157
+ }
142
158
  changed = true;
143
159
  }
144
160
  // Heal a defaultModel that no longer exists in the refreshed list (e.g. a deprecated snapshot).
@@ -20,8 +20,17 @@ export interface KeyLoginProvider {
20
20
  * accept a reasoning param. Copied into the created provider config by `enrichProviderFromCatalog`,
21
21
  * so the classification actually gates the sidecars (matching is tolerant of an Ollama ":size" tag).
22
22
  */
23
+ reasoningEfforts?: string[];
24
+ modelReasoningEfforts?: Record<string, string[]>;
25
+ reasoningEffortMap?: Record<string, string>;
26
+ modelReasoningEffortMap?: Record<string, Record<string, string>>;
23
27
  noVisionModels?: string[];
24
28
  noReasoningModels?: string[];
29
+ noTemperatureModels?: string[];
30
+ noTopPModels?: string[];
31
+ noPenaltyModels?: string[];
32
+ autoToolChoiceOnlyModels?: string[];
33
+ preserveReasoningContentModels?: string[];
25
34
  }
26
35
 
27
36
  export const KEY_LOGIN_PROVIDERS: Record<string, KeyLoginProvider> = deriveKeyLoginMap();
@@ -37,8 +46,26 @@ export function enrichProviderFromCatalog(name: string, prov: OcxProviderConfig)
37
46
  if (!e) return;
38
47
  if (!prov.models && e.models) prov.models = [...e.models];
39
48
  if (!prov.defaultModel && e.defaultModel) prov.defaultModel = e.defaultModel;
49
+ if (!prov.reasoningEfforts && e.reasoningEfforts) prov.reasoningEfforts = [...e.reasoningEfforts];
50
+ if (!prov.modelReasoningEfforts && e.modelReasoningEfforts) prov.modelReasoningEfforts = cloneRecordOfArrays(e.modelReasoningEfforts);
51
+ if (!prov.reasoningEffortMap && e.reasoningEffortMap) prov.reasoningEffortMap = { ...e.reasoningEffortMap };
52
+ if (!prov.modelReasoningEffortMap && e.modelReasoningEffortMap) prov.modelReasoningEffortMap = cloneNestedRecord(e.modelReasoningEffortMap);
40
53
  if (!prov.noVisionModels && e.noVisionModels) prov.noVisionModels = [...e.noVisionModels];
41
54
  if (!prov.noReasoningModels && e.noReasoningModels) prov.noReasoningModels = [...e.noReasoningModels];
55
+ if (!prov.noTemperatureModels && e.noTemperatureModels) prov.noTemperatureModels = [...e.noTemperatureModels];
56
+ if (!prov.noTopPModels && e.noTopPModels) prov.noTopPModels = [...e.noTopPModels];
57
+ if (!prov.noPenaltyModels && e.noPenaltyModels) prov.noPenaltyModels = [...e.noPenaltyModels];
58
+ if (!prov.autoToolChoiceOnlyModels && e.autoToolChoiceOnlyModels) prov.autoToolChoiceOnlyModels = [...e.autoToolChoiceOnlyModels];
59
+ if (!prov.preserveReasoningContentModels && e.preserveReasoningContentModels) prov.preserveReasoningContentModels = [...e.preserveReasoningContentModels];
60
+ }
61
+
62
+
63
+ function cloneRecordOfArrays(input: Record<string, string[]>): Record<string, string[]> {
64
+ return Object.fromEntries(Object.entries(input).map(([key, value]) => [key, [...value]]));
65
+ }
66
+
67
+ function cloneNestedRecord(input: Record<string, Record<string, string>>): Record<string, Record<string, string>> {
68
+ return Object.fromEntries(Object.entries(input).map(([key, value]) => [key, { ...value }]));
42
69
  }
43
70
 
44
71
  export function isKeyLoginProvider(name: string): boolean {
@@ -8,8 +8,17 @@ export interface DerivedKeyLoginProvider {
8
8
  dashboardUrl: string;
9
9
  models?: string[];
10
10
  defaultModel?: string;
11
+ reasoningEfforts?: string[];
12
+ modelReasoningEfforts?: Record<string, string[]>;
13
+ reasoningEffortMap?: Record<string, string>;
14
+ modelReasoningEffortMap?: Record<string, Record<string, string>>;
11
15
  noVisionModels?: string[];
12
16
  noReasoningModels?: string[];
17
+ noTemperatureModels?: string[];
18
+ noTopPModels?: string[];
19
+ noPenaltyModels?: string[];
20
+ autoToolChoiceOnlyModels?: string[];
21
+ preserveReasoningContentModels?: string[];
13
22
  }
14
23
 
15
24
  export interface DerivedInitProvider {
@@ -38,6 +47,14 @@ export function listRegistryEntries(): readonly ProviderRegistryEntry[] {
38
47
  return PROVIDER_REGISTRY;
39
48
  }
40
49
 
50
+ function cloneRecordOfArrays(input: Record<string, string[]>): Record<string, string[]> {
51
+ return Object.fromEntries(Object.entries(input).map(([key, value]) => [key, [...value]]));
52
+ }
53
+
54
+ function cloneNestedRecord(input: Record<string, Record<string, string>>): Record<string, Record<string, string>> {
55
+ return Object.fromEntries(Object.entries(input).map(([key, value]) => [key, { ...value }]));
56
+ }
57
+
41
58
  export function providerConfigSeed(entry: ProviderRegistryEntry): OcxProviderConfig {
42
59
  return {
43
60
  adapter: entry.adapter,
@@ -45,8 +62,17 @@ export function providerConfigSeed(entry: ProviderRegistryEntry): OcxProviderCon
45
62
  authMode: entry.authKind === "local" ? undefined : entry.authKind,
46
63
  ...(entry.defaultModel ? { defaultModel: entry.defaultModel } : {}),
47
64
  ...(entry.models ? { models: [...entry.models] } : {}),
65
+ ...(entry.reasoningEfforts ? { reasoningEfforts: [...entry.reasoningEfforts] } : {}),
66
+ ...(entry.modelReasoningEfforts ? { modelReasoningEfforts: cloneRecordOfArrays(entry.modelReasoningEfforts) } : {}),
67
+ ...(entry.reasoningEffortMap ? { reasoningEffortMap: { ...entry.reasoningEffortMap } } : {}),
68
+ ...(entry.modelReasoningEffortMap ? { modelReasoningEffortMap: cloneNestedRecord(entry.modelReasoningEffortMap) } : {}),
48
69
  ...(entry.noVisionModels ? { noVisionModels: [...entry.noVisionModels] } : {}),
49
70
  ...(entry.noReasoningModels ? { noReasoningModels: [...entry.noReasoningModels] } : {}),
71
+ ...(entry.noTemperatureModels ? { noTemperatureModels: [...entry.noTemperatureModels] } : {}),
72
+ ...(entry.noTopPModels ? { noTopPModels: [...entry.noTopPModels] } : {}),
73
+ ...(entry.noPenaltyModels ? { noPenaltyModels: [...entry.noPenaltyModels] } : {}),
74
+ ...(entry.autoToolChoiceOnlyModels ? { autoToolChoiceOnlyModels: [...entry.autoToolChoiceOnlyModels] } : {}),
75
+ ...(entry.preserveReasoningContentModels ? { preserveReasoningContentModels: [...entry.preserveReasoningContentModels] } : {}),
50
76
  };
51
77
  }
52
78
 
@@ -62,8 +88,17 @@ export function deriveKeyLoginMap(): Record<string, DerivedKeyLoginProvider> {
62
88
  dashboardUrl: entry.dashboardUrl,
63
89
  ...(entry.models ? { models: [...entry.models] } : {}),
64
90
  ...(entry.defaultModel ? { defaultModel: entry.defaultModel } : {}),
91
+ ...(entry.reasoningEfforts ? { reasoningEfforts: [...entry.reasoningEfforts] } : {}),
92
+ ...(entry.modelReasoningEfforts ? { modelReasoningEfforts: cloneRecordOfArrays(entry.modelReasoningEfforts) } : {}),
93
+ ...(entry.reasoningEffortMap ? { reasoningEffortMap: { ...entry.reasoningEffortMap } } : {}),
94
+ ...(entry.modelReasoningEffortMap ? { modelReasoningEffortMap: cloneNestedRecord(entry.modelReasoningEffortMap) } : {}),
65
95
  ...(entry.noVisionModels ? { noVisionModels: [...entry.noVisionModels] } : {}),
66
96
  ...(entry.noReasoningModels ? { noReasoningModels: [...entry.noReasoningModels] } : {}),
97
+ ...(entry.noTemperatureModels ? { noTemperatureModels: [...entry.noTemperatureModels] } : {}),
98
+ ...(entry.noTopPModels ? { noTopPModels: [...entry.noTopPModels] } : {}),
99
+ ...(entry.noPenaltyModels ? { noPenaltyModels: [...entry.noPenaltyModels] } : {}),
100
+ ...(entry.autoToolChoiceOnlyModels ? { autoToolChoiceOnlyModels: [...entry.autoToolChoiceOnlyModels] } : {}),
101
+ ...(entry.preserveReasoningContentModels ? { preserveReasoningContentModels: [...entry.preserveReasoningContentModels] } : {}),
67
102
  };
68
103
  }
69
104
  return out;
@@ -14,8 +14,17 @@ export interface ProviderRegistryEntry {
14
14
  dashboardUrl?: string;
15
15
  defaultModel?: string;
16
16
  models?: string[];
17
+ reasoningEfforts?: string[];
18
+ modelReasoningEfforts?: Record<string, string[]>;
19
+ reasoningEffortMap?: Record<string, string>;
20
+ modelReasoningEffortMap?: Record<string, Record<string, string>>;
17
21
  noVisionModels?: string[];
18
22
  noReasoningModels?: string[];
23
+ noTemperatureModels?: string[];
24
+ noTopPModels?: string[];
25
+ noPenaltyModels?: string[];
26
+ autoToolChoiceOnlyModels?: string[];
27
+ preserveReasoningContentModels?: string[];
19
28
  oauthId?: string;
20
29
  jawcodeBundle?: string;
21
30
  extraMetadataAliases?: string[];
@@ -24,9 +33,32 @@ export interface ProviderRegistryEntry {
24
33
 
25
34
  export type ProviderConfigSeed = Pick<
26
35
  OcxProviderConfig,
27
- "adapter" | "baseUrl" | "authMode" | "defaultModel" | "models" | "noVisionModels" | "noReasoningModels"
36
+ "adapter" | "baseUrl" | "authMode" | "defaultModel" | "models"
37
+ | "reasoningEfforts" | "modelReasoningEfforts" | "reasoningEffortMap" | "modelReasoningEffortMap"
38
+ | "noVisionModels" | "noReasoningModels" | "noTemperatureModels" | "noTopPModels" | "noPenaltyModels"
39
+ | "autoToolChoiceOnlyModels" | "preserveReasoningContentModels"
28
40
  >;
29
41
 
42
+
43
+ const ZAI_GLM_52_MODELS = ["glm-5.2", "glm-5.2[1m]"];
44
+ const ZAI_GLM_52_REASONING_EFFORTS = ["low", "medium", "high", "xhigh"];
45
+ const ZAI_GLM_52_REASONING_MAP: Record<string, string> = {
46
+ none: "none",
47
+ minimal: "none",
48
+ low: "high",
49
+ medium: "high",
50
+ high: "high",
51
+ xhigh: "max",
52
+ max: "max",
53
+ };
54
+ const KIMI_THINKING_MODELS = ["kimi-k2.7-code", "kimi-k2.7-code-highspeed", "kimi-k2.6", "kimi-k2.5", "kimi-k2-0905-preview"];
55
+ const KIMI_LOCKED_PARAMETER_MODELS = ["kimi-k2.7-code", "kimi-k2.7-code-highspeed", "kimi-k2.6", "kimi-k2.5"];
56
+ const NEURALWATT_REASONING_HISTORY_MODELS = [
57
+ "glm-5.2",
58
+ "moonshotai/Kimi-K2.5", "kimi-k2.6", "kimi-k2.7-code",
59
+ "qwen3.5-397b", "qwen3.6-35b",
60
+ ];
61
+
30
62
  export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
31
63
  {
32
64
  id: "openai",
@@ -75,11 +107,72 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
75
107
  oauthId: "kimi",
76
108
  jawcodeBundle: "moonshot",
77
109
  note: "Log in with your Kimi account",
78
- models: ["kimi-k2.6", "kimi-k2.5"],
79
- defaultModel: "kimi-k2.6",
110
+ models: ["kimi-k2.7-code", "kimi-k2.7-code-highspeed", "kimi-k2.6", "kimi-k2.5"],
111
+ defaultModel: "kimi-k2.7-code",
112
+ // Kimi thinking is controlled by Kimi's `thinking` extension, not OpenAI `reasoning_effort`.
113
+ noReasoningModels: KIMI_THINKING_MODELS,
114
+ modelReasoningEfforts: Object.fromEntries(KIMI_THINKING_MODELS.map(id => [id, []])),
115
+ noTemperatureModels: KIMI_LOCKED_PARAMETER_MODELS,
116
+ noTopPModels: KIMI_LOCKED_PARAMETER_MODELS,
117
+ noPenaltyModels: KIMI_LOCKED_PARAMETER_MODELS,
118
+ autoToolChoiceOnlyModels: ["kimi-k2.7-code", "kimi-k2.7-code-highspeed"],
119
+ preserveReasoningContentModels: KIMI_THINKING_MODELS,
80
120
  },
81
121
  { 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" },
82
- { id: "opencode-go", label: "opencode go", adapter: "openai-chat", baseUrl: "https://opencode.ai/zen/go/v1", authKind: "key", featured: true, dashboardUrl: "https://opencode.ai/auth", defaultModel: "kimi-k2.6", jawcodeBundle: "opencode-go", note: "GLM, DeepSeek, Kimi, Qwen, MiMo…" },
122
+ {
123
+ id: "opencode-go", label: "opencode go", adapter: "openai-chat", baseUrl: "https://opencode.ai/zen/go/v1",
124
+ authKind: "key", featured: true, dashboardUrl: "https://opencode.ai/auth", defaultModel: "kimi-k2.7-code",
125
+ jawcodeBundle: "opencode-go", note: "GLM, DeepSeek, Kimi, Qwen, MiMo…",
126
+ modelReasoningEfforts: {
127
+ "glm-5.2": ZAI_GLM_52_REASONING_EFFORTS,
128
+ "kimi-k2.7-code": [],
129
+ "kimi-k2.7-code-highspeed": [],
130
+ },
131
+ modelReasoningEffortMap: { "glm-5.2": ZAI_GLM_52_REASONING_MAP },
132
+ noReasoningModels: ["kimi-k2.7-code", "kimi-k2.7-code-highspeed"],
133
+ noTemperatureModels: ["kimi-k2.7-code", "kimi-k2.7-code-highspeed"],
134
+ noTopPModels: ["kimi-k2.7-code", "kimi-k2.7-code-highspeed"],
135
+ noPenaltyModels: ["kimi-k2.7-code", "kimi-k2.7-code-highspeed"],
136
+ autoToolChoiceOnlyModels: ["kimi-k2.7-code", "kimi-k2.7-code-highspeed"],
137
+ preserveReasoningContentModels: ["glm-5.2", "kimi-k2.7-code", "kimi-k2.7-code-highspeed"],
138
+ },
139
+ {
140
+ id: "neuralwatt",
141
+ label: "Neuralwatt Cloud",
142
+ adapter: "openai-chat",
143
+ baseUrl: "https://api.neuralwatt.com/v1",
144
+ authKind: "key",
145
+ dashboardUrl: "https://portal.neuralwatt.com",
146
+ defaultModel: "glm-5.2",
147
+ models: [
148
+ "glm-5.2", "glm-5.2-fast",
149
+ "moonshotai/Kimi-K2.5", "kimi-k2.5-fast", "kimi-k2.6", "kimi-k2.6-fast",
150
+ "kimi-k2.7-code",
151
+ "qwen3.5-397b", "qwen3.5-397b-fast", "qwen3.6-35b", "qwen3.6-35b-fast",
152
+ ],
153
+ // Neuralwatt's /v1/models metadata is authoritative; these static hints are the offline fallback.
154
+ modelReasoningEfforts: {
155
+ "glm-5.2": ZAI_GLM_52_REASONING_EFFORTS,
156
+ "glm-5.2-fast": [],
157
+ "moonshotai/Kimi-K2.5": [],
158
+ "kimi-k2.5-fast": [],
159
+ "kimi-k2.6": [],
160
+ "kimi-k2.6-fast": [],
161
+ "kimi-k2.7-code": [],
162
+ "qwen3.5-397b": ["low", "medium", "high"],
163
+ "qwen3.5-397b-fast": [],
164
+ "qwen3.6-35b": ["low", "medium", "high"],
165
+ "qwen3.6-35b-fast": [],
166
+ },
167
+ modelReasoningEffortMap: { "glm-5.2": ZAI_GLM_52_REASONING_MAP },
168
+ noReasoningModels: ["glm-5.2-fast", "kimi-k2.5-fast", "kimi-k2.6-fast", "qwen3.5-397b-fast", "qwen3.6-35b-fast"],
169
+ noVisionModels: ["glm-5.2", "glm-5.2-fast", "qwen3.5-397b", "qwen3.5-397b-fast"],
170
+ noTemperatureModels: ["kimi-k2.7-code"],
171
+ noTopPModels: ["kimi-k2.7-code"],
172
+ noPenaltyModels: ["kimi-k2.7-code"],
173
+ autoToolChoiceOnlyModels: ["kimi-k2.7-code"],
174
+ preserveReasoningContentModels: NEURALWATT_REASONING_HISTORY_MODELS,
175
+ },
83
176
  { id: "openrouter", label: "OpenRouter", adapter: "openai-chat", baseUrl: "https://openrouter.ai/api/v1", authKind: "key", featured: true, dashboardUrl: "https://openrouter.ai/keys", jawcodeBundle: "openrouter" },
84
177
  { id: "groq", label: "Groq", adapter: "openai-chat", baseUrl: "https://api.groq.com/openai/v1", authKind: "key", featured: true, dashboardUrl: "https://console.groq.com/keys" },
85
178
  { id: "google", label: "Google Gemini", adapter: "google", baseUrl: "https://generativelanguage.googleapis.com", authKind: "key", featured: true, dashboardUrl: "https://aistudio.google.com/apikey", defaultModel: "gemini-3-pro", jawcodeBundle: "google", extraMetadataAliases: ["gemini"] },
@@ -92,11 +185,30 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
92
185
  { id: "together", label: "Together", baseUrl: "https://api.together.xyz/v1", adapter: "openai-chat", authKind: "key", dashboardUrl: "https://api.together.xyz/settings/api-keys" },
93
186
  { id: "fireworks", label: "Fireworks", baseUrl: "https://api.fireworks.ai/inference/v1", adapter: "openai-chat", authKind: "key", dashboardUrl: "https://fireworks.ai/account/api-keys" },
94
187
  { id: "firepass", label: "Fire Pass (Fireworks Kimi)", baseUrl: "https://api.fireworks.ai/inference/v1", adapter: "openai-chat", authKind: "key", dashboardUrl: "https://fireworks.ai/account/api-keys" },
95
- { id: "moonshot", label: "Moonshot (Kimi API)", baseUrl: "https://api.moonshot.ai/v1", adapter: "openai-chat", authKind: "key", dashboardUrl: "https://platform.moonshot.ai/console/api-keys", defaultModel: "kimi-k2-0905-preview", jawcodeBundle: "moonshot" },
188
+ {
189
+ id: "moonshot", label: "Moonshot (Kimi API)", baseUrl: "https://api.moonshot.ai/v1", adapter: "openai-chat", authKind: "key",
190
+ dashboardUrl: "https://platform.moonshot.ai/console/api-keys", defaultModel: "kimi-k2.7-code", jawcodeBundle: "moonshot",
191
+ models: ["kimi-k2.7-code", "kimi-k2.7-code-highspeed", "kimi-k2.6", "kimi-k2.5", "kimi-k2-0905-preview"],
192
+ noReasoningModels: KIMI_THINKING_MODELS,
193
+ modelReasoningEfforts: Object.fromEntries(KIMI_THINKING_MODELS.map(id => [id, []])),
194
+ noTemperatureModels: KIMI_LOCKED_PARAMETER_MODELS,
195
+ noTopPModels: KIMI_LOCKED_PARAMETER_MODELS,
196
+ noPenaltyModels: KIMI_LOCKED_PARAMETER_MODELS,
197
+ autoToolChoiceOnlyModels: ["kimi-k2.7-code", "kimi-k2.7-code-highspeed"],
198
+ preserveReasoningContentModels: KIMI_THINKING_MODELS,
199
+ },
96
200
  { id: "huggingface", label: "Hugging Face", baseUrl: "https://router.huggingface.co/v1", adapter: "openai-chat", authKind: "key", dashboardUrl: "https://huggingface.co/settings/tokens" },
97
201
  { id: "nvidia", label: "NVIDIA NIM", baseUrl: "https://integrate.api.nvidia.com/v1", adapter: "openai-chat", authKind: "key", dashboardUrl: "https://build.nvidia.com" },
98
202
  { id: "venice", label: "Venice", baseUrl: "https://api.venice.ai/api/v1", adapter: "openai-chat", authKind: "key", dashboardUrl: "https://venice.ai/settings/api" },
99
- { id: "zai", label: "Z.AI (GLM Coding)", baseUrl: "https://api.z.ai/api/coding/paas/v4", adapter: "openai-chat", authKind: "key", dashboardUrl: "https://z.ai/manage-apikey/apikey-list", defaultModel: "glm-4.6" },
203
+ {
204
+ id: "zai", label: "Z.AI (GLM Coding)", baseUrl: "https://api.z.ai/api/coding/paas/v4", adapter: "openai-chat", authKind: "key",
205
+ dashboardUrl: "https://z.ai/manage-apikey/apikey-list", defaultModel: "glm-5.2",
206
+ models: ["glm-5.2", "glm-5.2[1m]", "glm-5.1", "glm-5", "glm-4.6"],
207
+ noVisionModels: ZAI_GLM_52_MODELS,
208
+ modelReasoningEfforts: Object.fromEntries(ZAI_GLM_52_MODELS.map(id => [id, ZAI_GLM_52_REASONING_EFFORTS])),
209
+ modelReasoningEffortMap: Object.fromEntries(ZAI_GLM_52_MODELS.map(id => [id, ZAI_GLM_52_REASONING_MAP])),
210
+ preserveReasoningContentModels: ZAI_GLM_52_MODELS,
211
+ },
100
212
  { id: "nanogpt", label: "NanoGPT", baseUrl: "https://nano-gpt.com/api/v1", adapter: "openai-chat", authKind: "key", dashboardUrl: "https://nano-gpt.com/api" },
101
213
  { id: "synthetic", label: "Synthetic", baseUrl: "https://api.synthetic.new/openai/v1", adapter: "openai-chat", authKind: "key", dashboardUrl: "https://synthetic.new" },
102
214
  { id: "qwen-portal", label: "Qwen Portal", baseUrl: "https://portal.qwen.ai/v1", adapter: "openai-chat", authKind: "key", dashboardUrl: "https://portal.qwen.ai" },
@@ -125,7 +237,18 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
125
237
  { id: "mistral", label: "Mistral", baseUrl: "https://api.mistral.ai/v1", adapter: "openai-chat", authKind: "key", dashboardUrl: "https://console.mistral.ai/api-keys", defaultModel: "codestral-latest" },
126
238
  { id: "minimax", label: "MiniMax", baseUrl: "https://api.minimax.io/v1", adapter: "openai-chat", authKind: "key", dashboardUrl: "https://platform.minimax.io", defaultModel: "MiniMax-M2.5", jawcodeBundle: "minimax", metadataModelIdNormalize: "case-insensitive" },
127
239
  { id: "minimax-cn", label: "MiniMax (CN)", baseUrl: "https://api.minimaxi.com/v1", adapter: "openai-chat", authKind: "key", dashboardUrl: "https://platform.minimaxi.com", defaultModel: "MiniMax-M2.5", jawcodeBundle: "minimax", metadataModelIdNormalize: "case-insensitive" },
128
- { id: "kimi-code", label: "Kimi (coding)", baseUrl: "https://api.kimi.com/coding/v1", adapter: "openai-chat", authKind: "key", dashboardUrl: "https://platform.moonshot.cn/console/api-keys", defaultModel: "kimi-k2.5" },
240
+ {
241
+ id: "kimi-code", label: "Kimi (coding)", baseUrl: "https://api.kimi.com/coding/v1", adapter: "openai-chat", authKind: "key",
242
+ dashboardUrl: "https://platform.moonshot.cn/console/api-keys", defaultModel: "kimi-k2.7-code",
243
+ models: ["kimi-k2.7-code", "kimi-k2.7-code-highspeed", "kimi-k2.6", "kimi-k2.5"],
244
+ noReasoningModels: KIMI_THINKING_MODELS,
245
+ modelReasoningEfforts: Object.fromEntries(KIMI_THINKING_MODELS.map(id => [id, []])),
246
+ noTemperatureModels: KIMI_LOCKED_PARAMETER_MODELS,
247
+ noTopPModels: KIMI_LOCKED_PARAMETER_MODELS,
248
+ noPenaltyModels: KIMI_LOCKED_PARAMETER_MODELS,
249
+ autoToolChoiceOnlyModels: ["kimi-k2.7-code", "kimi-k2.7-code-highspeed"],
250
+ preserveReasoningContentModels: KIMI_THINKING_MODELS,
251
+ },
129
252
  { id: "opencode-zen", label: "opencode zen", baseUrl: "https://opencode.ai/zen/v1", adapter: "openai-chat", authKind: "key", dashboardUrl: "https://opencode.ai/auth" },
130
253
  { id: "vercel-ai-gateway", label: "Vercel AI Gateway", baseUrl: "https://ai-gateway.vercel.sh/v1", adapter: "openai-chat", authKind: "key", dashboardUrl: "https://vercel.com/dashboard" },
131
254
  { id: "xiaomi", label: "Xiaomi MiMo", baseUrl: "https://api.xiaomimimo.com/anthropic", adapter: "anthropic", authKind: "key", dashboardUrl: "https://xiaomimimo.com", defaultModel: "mimo-v2.5-pro" },
@@ -0,0 +1,102 @@
1
+ import type { OcxProviderConfig } from "./types";
2
+ import { modelInList } from "./types";
3
+
4
+ export const CODEX_REASONING_LEVELS: { effort: string; description: string }[] = [
5
+ { effort: "low", description: "Fast responses with lighter reasoning" },
6
+ { effort: "medium", description: "Balances speed and reasoning depth" },
7
+ { effort: "high", description: "Greater reasoning depth for complex problems" },
8
+ { effort: "xhigh", description: "Extended reasoning for the hardest problems" },
9
+ ];
10
+
11
+ const CODEX_REASONING_ORDER = CODEX_REASONING_LEVELS.map(l => l.effort);
12
+ const CODEX_REASONING_SET = new Set(CODEX_REASONING_ORDER);
13
+
14
+ export function modelRecordValue<T>(record: Record<string, T> | undefined, modelId: string): T | undefined {
15
+ if (!record) return undefined;
16
+ if (Object.prototype.hasOwnProperty.call(record, modelId)) return record[modelId];
17
+ const colon = modelId.indexOf(":");
18
+ if (colon > 0) {
19
+ const family = modelId.slice(0, colon);
20
+ if (Object.prototype.hasOwnProperty.call(record, family)) return record[family];
21
+ }
22
+ const folded = modelId.toLowerCase();
23
+ for (const [key, value] of Object.entries(record)) {
24
+ if (key.toLowerCase() === folded) return value;
25
+ }
26
+ return undefined;
27
+ }
28
+
29
+ export function sanitizeCodexReasoningEfforts(efforts: readonly string[] | undefined): string[] | undefined {
30
+ if (efforts === undefined) return undefined;
31
+ const seen = new Set<string>();
32
+ const out: string[] = [];
33
+ for (const effort of efforts) {
34
+ if (!CODEX_REASONING_SET.has(effort) || seen.has(effort)) continue;
35
+ seen.add(effort);
36
+ out.push(effort);
37
+ }
38
+ return out.sort((a, b) => CODEX_REASONING_ORDER.indexOf(a) - CODEX_REASONING_ORDER.indexOf(b));
39
+ }
40
+
41
+ /**
42
+ * Provider/model configured reasoning levels for the Codex catalog. `undefined` means “no override”,
43
+ * while an empty array means “intentionally expose no effort control for this model”.
44
+ */
45
+ export function configuredReasoningEfforts(provider: OcxProviderConfig, modelId: string): string[] | undefined {
46
+ if (modelInList(provider.noReasoningModels, modelId)) return [];
47
+ const modelEfforts = modelRecordValue(provider.modelReasoningEfforts, modelId);
48
+ if (modelEfforts !== undefined) return sanitizeCodexReasoningEfforts(modelEfforts) ?? [];
49
+ if (provider.reasoningEfforts !== undefined) return sanitizeCodexReasoningEfforts(provider.reasoningEfforts) ?? [];
50
+ return undefined;
51
+ }
52
+
53
+ function requestToCodexEffort(requested: string): string | undefined {
54
+ if (requested === "none") return undefined;
55
+ if (requested === "minimal") return "low";
56
+ if (requested === "max") return "xhigh";
57
+ return CODEX_REASONING_SET.has(requested) ? requested : undefined;
58
+ }
59
+
60
+ function clampToSupportedCodexEffort(requested: string, supported: readonly string[]): string | undefined {
61
+ if (supported.length === 0) return undefined;
62
+ const codex = requestToCodexEffort(requested);
63
+ if (!codex) return undefined;
64
+ if (supported.includes(codex)) return codex;
65
+
66
+ const requestedRank = CODEX_REASONING_ORDER.indexOf(codex);
67
+ let best = supported[0];
68
+ let bestRank = CODEX_REASONING_ORDER.indexOf(best);
69
+ for (const effort of supported) {
70
+ const rank = CODEX_REASONING_ORDER.indexOf(effort);
71
+ if (rank <= requestedRank && rank >= bestRank) {
72
+ best = effort;
73
+ bestRank = rank;
74
+ }
75
+ }
76
+ // If every supported tier is above the requested tier, choose the lowest supported tier.
77
+ return best;
78
+ }
79
+
80
+ export function reasoningEffortMapFor(provider: OcxProviderConfig, modelId: string): Record<string, string> | undefined {
81
+ return modelRecordValue(provider.modelReasoningEffortMap, modelId) ?? provider.reasoningEffortMap;
82
+ }
83
+
84
+ /**
85
+ * Translate Codex's reasoning label into the provider's real wire value. The Codex catalog must only
86
+ * advertise labels Codex itself accepts (`low`/`medium`/`high`/`xhigh`), but some upstreams use
87
+ * different values (`max`) or a smaller subset (`low`/`medium`/`high`).
88
+ */
89
+ export function mapReasoningEffort(provider: OcxProviderConfig, modelId: string, requested: string | undefined): string | undefined {
90
+ if (!requested) return undefined;
91
+ if (modelInList(provider.noReasoningModels, modelId)) return undefined;
92
+
93
+ const wireMap = reasoningEffortMapFor(provider, modelId);
94
+ if (wireMap && Object.prototype.hasOwnProperty.call(wireMap, requested)) return wireMap[requested];
95
+
96
+ const supported = configuredReasoningEfforts(provider, modelId);
97
+ const codexEffort = supported !== undefined ? clampToSupportedCodexEffort(requested, supported) : requestToCodexEffort(requested);
98
+ if (!codexEffort) return undefined;
99
+
100
+ if (wireMap && Object.prototype.hasOwnProperty.call(wireMap, codexEffort)) return wireMap[codexEffort];
101
+ return codexEffort;
102
+ }
@@ -188,7 +188,7 @@ function findToolNameById(messages: OcxMessage[], callId: string): string {
188
188
  return "";
189
189
  }
190
190
 
191
- const REASONING_EFFORTS = new Set(["minimal", "low", "medium", "high", "xhigh", "max"]);
191
+ const REASONING_EFFORTS = new Set(["none", "minimal", "low", "medium", "high", "xhigh", "max"]);
192
192
 
193
193
  export function parseRequest(body: unknown): OcxParsedRequest {
194
194
  const parsed = responsesRequestSchema.safeParse(body);
package/src/server.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { existsSync } from "node:fs";
1
+ import { existsSync, readFileSync } from "node:fs";
2
2
  import { extname, join } from "node:path";
3
3
  import { createAnthropicAdapter } from "./adapters/anthropic";
4
4
  import { createAzureAdapter } from "./adapters/azure";
@@ -32,7 +32,15 @@ import { enrichProviderFromCatalog, listKeyLoginProviders } from "./oauth/key-pr
32
32
  import { deriveProviderPresets } from "./providers/derive";
33
33
  import type { OcxConfig, OcxProviderConfig } from "./types";
34
34
 
35
- const VERSION = "0.0.1";
35
+ // Single source of truth = package.json (../ from src/), so /healthz + the GUI badge match the
36
+ // installed npm version instead of a stale hardcode.
37
+ const VERSION = (() => {
38
+ try {
39
+ return JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version as string;
40
+ } catch {
41
+ return "0.0.0";
42
+ }
43
+ })();
36
44
 
37
45
  const MIME_TYPES: Record<string, string> = {
38
46
  ".html": "text/html", ".js": "application/javascript", ".css": "text/css",
@@ -522,6 +530,15 @@ async function handleManagementAPI(req: Request, url: URL, config: OcxConfig): P
522
530
  return jsonResponse({ success: true });
523
531
  }
524
532
 
533
+ if (url.pathname === "/api/stop" && req.method === "POST") {
534
+ const { restoreNativeCodex } = await import("./codex-inject");
535
+ const { stopServiceIfInstalled } = await import("./service");
536
+ stopServiceIfInstalled();
537
+ restoreNativeCodex();
538
+ setTimeout(() => process.exit(0), 200);
539
+ return jsonResponse({ success: true, message: "Proxy stopping, native Codex restored." });
540
+ }
541
+
525
542
  return null;
526
543
  }
527
544