@koreanhole/opencodex 2.95.10 → 2.97.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 +33 -6
- package/README.md +100 -31
- package/package.json +6 -2
- package/src/adapters/cli-agent/acp-client.ts +545 -0
- package/src/adapters/cli-agent/cline/acp.ts +15 -279
- package/src/adapters/cli-agent/cline/adapter.ts +3 -3
- package/src/adapters/cli-agent/cline/live-models.ts +6 -5
- package/src/adapters/cli-agent/cline/stream.ts +6 -37
- package/src/adapters/cli-agent/gemini/acp.ts +656 -0
- package/src/adapters/cli-agent/gemini/adapter.ts +253 -0
- package/src/adapters/cli-agent/gemini/binary.ts +23 -0
- package/src/adapters/cli-agent/gemini/live-models.ts +154 -0
- package/src/adapters/cli-agent/gemini/model-metadata.ts +291 -0
- package/src/adapters/cli-agent/mcp-server.ts +29 -4
- package/src/adapters/cli-agent/tool-intent.ts +71 -0
- package/src/cli/index.ts +81 -19
- package/src/config.ts +50 -9
- package/src/index.ts +1 -0
- package/src/lib/codex-catalog.ts +318 -0
- package/src/lib/redact.ts +4 -1
- package/src/server/index.ts +46 -13
- package/src/server/messages.ts +9 -8
- package/src/server/responses.ts +126 -12
- package/src/types.ts +14 -0
package/src/config.ts
CHANGED
|
@@ -3,10 +3,13 @@ import { homedir } from "node:os";
|
|
|
3
3
|
import { dirname, join, resolve } from "node:path";
|
|
4
4
|
|
|
5
5
|
export const CLINE_PROVIDER_ID = "cline-cli";
|
|
6
|
+
export const GEMINI_PROVIDER_ID = "gemini-cli";
|
|
6
7
|
export const CLINE_BASE_URL = "https://api.cline.bot";
|
|
7
8
|
export const DEFAULT_PORT = 10_100;
|
|
8
9
|
export const DEFAULT_HOST = "127.0.0.1";
|
|
9
10
|
|
|
11
|
+
export type ProviderId = typeof CLINE_PROVIDER_ID | typeof GEMINI_PROVIDER_ID;
|
|
12
|
+
|
|
10
13
|
export interface ClineProviderConfig {
|
|
11
14
|
adapter: "cline-cli";
|
|
12
15
|
baseUrl: typeof CLINE_BASE_URL;
|
|
@@ -14,11 +17,19 @@ export interface ClineProviderConfig {
|
|
|
14
17
|
models: string[];
|
|
15
18
|
}
|
|
16
19
|
|
|
20
|
+
export interface GeminiProviderConfig {
|
|
21
|
+
adapter: "gemini-cli";
|
|
22
|
+
defaultModel: "default";
|
|
23
|
+
models: string[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type ProviderConfig = ClineProviderConfig | GeminiProviderConfig;
|
|
27
|
+
|
|
17
28
|
export interface RuntimeConfig {
|
|
18
29
|
port: number;
|
|
19
30
|
host: string;
|
|
20
|
-
defaultProvider:
|
|
21
|
-
providers: Record<
|
|
31
|
+
defaultProvider: ProviderId;
|
|
32
|
+
providers: Record<ProviderId, ProviderConfig>;
|
|
22
33
|
apiAuthToken?: string;
|
|
23
34
|
}
|
|
24
35
|
|
|
@@ -67,6 +78,11 @@ export function defaultConfig(): RuntimeConfig {
|
|
|
67
78
|
defaultModel: "default",
|
|
68
79
|
models: ["default"],
|
|
69
80
|
},
|
|
81
|
+
[GEMINI_PROVIDER_ID]: {
|
|
82
|
+
adapter: GEMINI_PROVIDER_ID,
|
|
83
|
+
defaultModel: "default",
|
|
84
|
+
models: ["default"],
|
|
85
|
+
},
|
|
70
86
|
},
|
|
71
87
|
};
|
|
72
88
|
}
|
|
@@ -85,7 +101,7 @@ function validPort(value: unknown): number {
|
|
|
85
101
|
return value;
|
|
86
102
|
}
|
|
87
103
|
|
|
88
|
-
function
|
|
104
|
+
function normalizeClineProvider(value: unknown): ClineProviderConfig {
|
|
89
105
|
const row = asRecord(value);
|
|
90
106
|
if (!row) throw new ConfigError("config.providers.cline-cli must be an object.");
|
|
91
107
|
if (row.adapter !== undefined && row.adapter !== CLINE_PROVIDER_ID) {
|
|
@@ -106,17 +122,42 @@ function normalizeProvider(value: unknown): ClineProviderConfig {
|
|
|
106
122
|
};
|
|
107
123
|
}
|
|
108
124
|
|
|
125
|
+
function normalizeGeminiProvider(value: unknown): GeminiProviderConfig {
|
|
126
|
+
const row = asRecord(value);
|
|
127
|
+
if (!row) throw new ConfigError("config.providers.gemini-cli must be an object.");
|
|
128
|
+
if (row.adapter !== undefined && row.adapter !== GEMINI_PROVIDER_ID) {
|
|
129
|
+
throw new ConfigError("config.providers.gemini-cli.adapter must be gemini-cli.");
|
|
130
|
+
}
|
|
131
|
+
const models = Array.isArray(row.models)
|
|
132
|
+
? [...new Set(row.models.filter((item): item is string => typeof item === "string" && item.length > 0))]
|
|
133
|
+
: ["default"];
|
|
134
|
+
if (!models.includes("default")) models.unshift("default");
|
|
135
|
+
return {
|
|
136
|
+
adapter: GEMINI_PROVIDER_ID,
|
|
137
|
+
defaultModel: "default",
|
|
138
|
+
models,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function providerId(value: unknown): ProviderId | undefined {
|
|
143
|
+
return value === CLINE_PROVIDER_ID || value === GEMINI_PROVIDER_ID ? value : undefined;
|
|
144
|
+
}
|
|
145
|
+
|
|
109
146
|
export function normalizeConfig(input: unknown): RuntimeConfig {
|
|
110
147
|
const row = asRecord(input);
|
|
111
148
|
if (!row) throw new ConfigError("config.json must contain a JSON object.");
|
|
112
|
-
|
|
149
|
+
let defaultProvider = providerId(row.defaultProvider);
|
|
150
|
+
if (row.defaultProvider !== undefined && !defaultProvider) {
|
|
113
151
|
console.warn(`[opencodex] ignoring unsupported defaultProvider ${JSON.stringify(row.defaultProvider)}; using cline-cli.`);
|
|
152
|
+
defaultProvider = CLINE_PROVIDER_ID;
|
|
114
153
|
}
|
|
115
154
|
const providers = asRecord(row.providers);
|
|
116
155
|
for (const key of Object.keys(providers ?? {})) {
|
|
117
|
-
if (key
|
|
156
|
+
if (!providerId(key)) console.warn(`[opencodex] ignoring unsupported provider ${JSON.stringify(key)}.`);
|
|
118
157
|
}
|
|
119
|
-
const
|
|
158
|
+
const defaults = defaultConfig().providers;
|
|
159
|
+
const cline = normalizeClineProvider(providers?.[CLINE_PROVIDER_ID] ?? defaults[CLINE_PROVIDER_ID]);
|
|
160
|
+
const gemini = normalizeGeminiProvider(providers?.[GEMINI_PROVIDER_ID] ?? defaults[GEMINI_PROVIDER_ID]);
|
|
120
161
|
const host = typeof row.host === "string" && row.host.trim() ? row.host.trim() : DEFAULT_HOST;
|
|
121
162
|
const apiAuthToken = typeof row.apiAuthToken === "string" && row.apiAuthToken.trim()
|
|
122
163
|
? row.apiAuthToken.trim()
|
|
@@ -124,8 +165,8 @@ export function normalizeConfig(input: unknown): RuntimeConfig {
|
|
|
124
165
|
return {
|
|
125
166
|
port: row.port === undefined ? DEFAULT_PORT : validPort(row.port),
|
|
126
167
|
host,
|
|
127
|
-
defaultProvider: CLINE_PROVIDER_ID,
|
|
128
|
-
providers: { [CLINE_PROVIDER_ID]:
|
|
168
|
+
defaultProvider: defaultProvider ?? CLINE_PROVIDER_ID,
|
|
169
|
+
providers: { [CLINE_PROVIDER_ID]: cline, [GEMINI_PROVIDER_ID]: gemini },
|
|
129
170
|
...(apiAuthToken ? { apiAuthToken } : {}),
|
|
130
171
|
};
|
|
131
172
|
}
|
|
@@ -143,7 +184,7 @@ export async function loadConfig(env: NodeJS.ProcessEnv = process.env): Promise<
|
|
|
143
184
|
try {
|
|
144
185
|
parsed = JSON.parse(text);
|
|
145
186
|
} catch {
|
|
146
|
-
throw new ConfigError(`${path} is not valid JSON. Repair it or remove it to recreate the
|
|
187
|
+
throw new ConfigError(`${path} is not valid JSON. Repair it or remove it to recreate the default config.`);
|
|
147
188
|
}
|
|
148
189
|
return normalizeConfig(parsed);
|
|
149
190
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { startServer, type RunningServer, type ServerOptions } from "./server";
|
|
2
2
|
export { loadConfig, saveConfig, defaultConfig } from "./config";
|
|
3
3
|
export { runClineTurn } from "./adapters/cli-agent/cline/adapter";
|
|
4
|
+
export { runGeminiTurn } from "./adapters/cli-agent/gemini/adapter";
|
|
4
5
|
export type {
|
|
5
6
|
AdapterEvent,
|
|
6
7
|
IncomingMeta,
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
import { chmod, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
4
|
+
import type { OcxProviderModel } from "../types";
|
|
5
|
+
import { discoverClineLiveModels } from "../adapters/cli-agent/cline/live-models";
|
|
6
|
+
import { discoverGeminiLiveModels } from "../adapters/cli-agent/gemini/live-models";
|
|
7
|
+
import type { ClineTurnDeps } from "../adapters/cli-agent/cline/adapter";
|
|
8
|
+
import type { GeminiTurnDeps } from "../adapters/cli-agent/gemini/adapter";
|
|
9
|
+
|
|
10
|
+
const CATALOG_FALLBACK_CONTEXT = 128_000;
|
|
11
|
+
const BASE_INSTRUCTIONS = "You are a helpful coding assistant.";
|
|
12
|
+
const MANAGED_PREFIXES = ["cline-cli/", "gemini-cli/"] as const;
|
|
13
|
+
|
|
14
|
+
export interface CodexCatalogModel {
|
|
15
|
+
slug: string;
|
|
16
|
+
display_name: string;
|
|
17
|
+
description: string;
|
|
18
|
+
default_reasoning_level: string;
|
|
19
|
+
supported_reasoning_levels: Array<{ effort: string; description: string }>;
|
|
20
|
+
shell_type: "unified_exec";
|
|
21
|
+
visibility: "list";
|
|
22
|
+
supported_in_api: true;
|
|
23
|
+
priority: number;
|
|
24
|
+
base_instructions: string;
|
|
25
|
+
web_search_tool_type: "text_and_image";
|
|
26
|
+
supports_search_tool: false;
|
|
27
|
+
tool_mode: "code_mode_only";
|
|
28
|
+
context_window: number;
|
|
29
|
+
max_context_window: number;
|
|
30
|
+
auto_compact_token_limit: number;
|
|
31
|
+
input_modalities: ["text"];
|
|
32
|
+
node_repl_disabled: false;
|
|
33
|
+
node_repl_auto_review_required: false;
|
|
34
|
+
include_plugin_usage_instructions: false;
|
|
35
|
+
include_apps_usage_instructions: false;
|
|
36
|
+
include_skills_usage_instructions: false;
|
|
37
|
+
supports_reasoning_summaries: boolean;
|
|
38
|
+
default_reasoning_summary: string;
|
|
39
|
+
support_verbosity: true;
|
|
40
|
+
default_verbosity: "low";
|
|
41
|
+
apply_patch_tool_type: "freeform";
|
|
42
|
+
truncation_policy: { mode: "tokens"; limit: number };
|
|
43
|
+
supports_parallel_tool_calls: true;
|
|
44
|
+
supports_image_detail_original: false;
|
|
45
|
+
experimental_supported_tools: [];
|
|
46
|
+
effective_context_window_percent: 95;
|
|
47
|
+
comp_hash: null;
|
|
48
|
+
multi_agent_version: "v1";
|
|
49
|
+
auto_review_model_override: null;
|
|
50
|
+
supports_reasoning_effort_updates: false;
|
|
51
|
+
use_responses_lite: false;
|
|
52
|
+
additional_speed_tiers: [];
|
|
53
|
+
service_tiers: [];
|
|
54
|
+
availability_nux: null;
|
|
55
|
+
upgrade: null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface CodexCatalogDocument {
|
|
59
|
+
fetched_at: string;
|
|
60
|
+
etag: null;
|
|
61
|
+
client_version: string;
|
|
62
|
+
identity: string;
|
|
63
|
+
models: CodexCatalogModel[];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface CodexCatalogDiscoveryOptions {
|
|
67
|
+
env?: NodeJS.ProcessEnv;
|
|
68
|
+
clineDeps?: ClineTurnDeps;
|
|
69
|
+
geminiDeps?: GeminiTurnDeps;
|
|
70
|
+
geminiBinary?: string;
|
|
71
|
+
geminiPackageRoot?: string;
|
|
72
|
+
now?: Date;
|
|
73
|
+
forceRefresh?: boolean;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface CodexCatalogModels {
|
|
77
|
+
models: CodexCatalogModel[];
|
|
78
|
+
warnings: string[];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export type CodexCatalogSyncResult =
|
|
82
|
+
| {
|
|
83
|
+
ok: true;
|
|
84
|
+
path: string;
|
|
85
|
+
configured: boolean;
|
|
86
|
+
written: boolean;
|
|
87
|
+
modelCount: number;
|
|
88
|
+
geminiModelCount: number;
|
|
89
|
+
reasoningModelCount: number;
|
|
90
|
+
reloadRequired: true;
|
|
91
|
+
warnings: string[];
|
|
92
|
+
}
|
|
93
|
+
| {
|
|
94
|
+
ok: false;
|
|
95
|
+
path?: string;
|
|
96
|
+
reason: "unsafe_path" | "malformed_catalog" | "write_failed" | "discovery_failed";
|
|
97
|
+
message: string;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
|
101
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
102
|
+
? value as Record<string, unknown>
|
|
103
|
+
: undefined;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function encodeRoutedId(id: string): string {
|
|
107
|
+
return id.replaceAll("/", "-");
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function buildCodexCatalogModel(providerId: "cline-cli" | "gemini-cli", model: OcxProviderModel, index: number): CodexCatalogModel {
|
|
111
|
+
const levels = (model.supportedReasoningLevels ?? []).map(level => ({
|
|
112
|
+
effort: level.effort,
|
|
113
|
+
description: level.description,
|
|
114
|
+
}));
|
|
115
|
+
const defaultLevel = model.defaultReasoningLevel ?? levels[0]?.effort ?? "low";
|
|
116
|
+
const isDefault = model.id === "default";
|
|
117
|
+
const providerLabel = providerId === "gemini-cli" ? "Gemini CLI" : "Cline CLI";
|
|
118
|
+
return {
|
|
119
|
+
slug: `${providerId}/${encodeRoutedId(model.id)}`,
|
|
120
|
+
display_name: model.name ?? (isDefault ? `${providerLabel} default` : model.id),
|
|
121
|
+
description: model.description ?? `Routed via opencodex -> ${providerLabel}.`,
|
|
122
|
+
default_reasoning_level: levels.length > 0 ? defaultLevel : "low",
|
|
123
|
+
supported_reasoning_levels: levels,
|
|
124
|
+
shell_type: "unified_exec",
|
|
125
|
+
visibility: "list",
|
|
126
|
+
supported_in_api: true,
|
|
127
|
+
priority: 10 + index,
|
|
128
|
+
base_instructions: BASE_INSTRUCTIONS,
|
|
129
|
+
web_search_tool_type: "text_and_image",
|
|
130
|
+
supports_search_tool: false,
|
|
131
|
+
tool_mode: "code_mode_only",
|
|
132
|
+
context_window: CATALOG_FALLBACK_CONTEXT,
|
|
133
|
+
max_context_window: CATALOG_FALLBACK_CONTEXT,
|
|
134
|
+
auto_compact_token_limit: Math.floor(CATALOG_FALLBACK_CONTEXT * 0.9),
|
|
135
|
+
input_modalities: ["text"],
|
|
136
|
+
node_repl_disabled: false,
|
|
137
|
+
node_repl_auto_review_required: false,
|
|
138
|
+
include_plugin_usage_instructions: false,
|
|
139
|
+
include_apps_usage_instructions: false,
|
|
140
|
+
include_skills_usage_instructions: false,
|
|
141
|
+
supports_reasoning_summaries: levels.length > 0,
|
|
142
|
+
default_reasoning_summary: levels.length > 0 ? "auto" : "none",
|
|
143
|
+
support_verbosity: true,
|
|
144
|
+
default_verbosity: "low",
|
|
145
|
+
apply_patch_tool_type: "freeform",
|
|
146
|
+
truncation_policy: { mode: "tokens", limit: 10_000 },
|
|
147
|
+
supports_parallel_tool_calls: true,
|
|
148
|
+
supports_image_detail_original: false,
|
|
149
|
+
experimental_supported_tools: [],
|
|
150
|
+
effective_context_window_percent: 95,
|
|
151
|
+
comp_hash: null,
|
|
152
|
+
multi_agent_version: "v1",
|
|
153
|
+
auto_review_model_override: null,
|
|
154
|
+
supports_reasoning_effort_updates: false,
|
|
155
|
+
use_responses_lite: false,
|
|
156
|
+
additional_speed_tiers: [],
|
|
157
|
+
service_tiers: [],
|
|
158
|
+
availability_nux: null,
|
|
159
|
+
upgrade: null,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function defaultModel(): OcxProviderModel {
|
|
164
|
+
return { id: "default" };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function normalizedModels(models: readonly OcxProviderModel[]): OcxProviderModel[] {
|
|
168
|
+
const seen = new Set<string>();
|
|
169
|
+
const out: OcxProviderModel[] = [];
|
|
170
|
+
for (const model of [defaultModel(), ...models]) {
|
|
171
|
+
if (!seen.has(model.id)) {
|
|
172
|
+
seen.add(model.id);
|
|
173
|
+
out.push(model);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return out;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export async function discoverCodexCatalogModels(options: CodexCatalogDiscoveryOptions = {}): Promise<CodexCatalogModels> {
|
|
180
|
+
const env = options.env ?? process.env;
|
|
181
|
+
const [cline, gemini] = await Promise.all([
|
|
182
|
+
discoverClineLiveModels({ env, ...(options.clineDeps ? { deps: options.clineDeps } : {}) }),
|
|
183
|
+
discoverGeminiLiveModels({
|
|
184
|
+
env,
|
|
185
|
+
...(options.geminiBinary ? { binary: options.geminiBinary } : {}),
|
|
186
|
+
...(options.geminiDeps ? { deps: options.geminiDeps } : {}),
|
|
187
|
+
...(options.geminiPackageRoot ? { packageRoot: options.geminiPackageRoot } : {}),
|
|
188
|
+
...(options.forceRefresh ? { forceRefresh: true } : {}),
|
|
189
|
+
}),
|
|
190
|
+
]);
|
|
191
|
+
const warnings: string[] = [];
|
|
192
|
+
if (!cline.ok && cline.reason !== "not_found" && env.OPENCODEX_CLINE_ACP?.trim() !== "0") warnings.push(`Cline: ${cline.detail}`);
|
|
193
|
+
if (!gemini.ok && gemini.reason !== "not_found" && env.OPENCODEX_GEMINI_ACP?.trim() !== "0") warnings.push(`Gemini: ${gemini.detail}`);
|
|
194
|
+
if (gemini.ok && gemini.metadataWarning) warnings.push(`Gemini metadata: ${gemini.metadataWarning}`);
|
|
195
|
+
const clineModels = normalizedModels(cline.ok ? cline.models.map(id => ({ id })) : []);
|
|
196
|
+
const geminiModels = normalizedModels(gemini.ok ? gemini.modelInfo : []);
|
|
197
|
+
return {
|
|
198
|
+
models: [
|
|
199
|
+
...clineModels.map((model, index) => buildCodexCatalogModel("cline-cli", model, index)),
|
|
200
|
+
...geminiModels.map((model, index) => buildCodexCatalogModel("gemini-cli", model, clineModels.length + index)),
|
|
201
|
+
],
|
|
202
|
+
warnings,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function buildCodexCatalogDocument(
|
|
207
|
+
models: CodexCatalogModel[],
|
|
208
|
+
now = new Date(),
|
|
209
|
+
preserved: CodexCatalogModel[] = [],
|
|
210
|
+
): CodexCatalogDocument {
|
|
211
|
+
return {
|
|
212
|
+
fetched_at: now.toISOString(),
|
|
213
|
+
etag: null,
|
|
214
|
+
client_version: "0.165.0",
|
|
215
|
+
identity: "opencodex",
|
|
216
|
+
models: [...preserved, ...models],
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function readRootTomlString(text: string, key: string): string | undefined {
|
|
221
|
+
const firstTable = text.search(/^\s*\[/m);
|
|
222
|
+
const root = firstTable < 0 ? text : text.slice(0, firstTable);
|
|
223
|
+
const match = new RegExp(`^\\s*${key}\\s*=\\s*"([^"]+)"`, "m").exec(root);
|
|
224
|
+
return match?.[1]?.trim() || undefined;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export async function writeCodexCatalog(
|
|
228
|
+
path: string,
|
|
229
|
+
document: CodexCatalogDocument,
|
|
230
|
+
): Promise<void> {
|
|
231
|
+
const parent = dirname(path);
|
|
232
|
+
await mkdir(parent, { recursive: true, mode: 0o700 });
|
|
233
|
+
const temporary = `${path}.${process.pid}.${crypto.randomUUID()}.tmp`;
|
|
234
|
+
try {
|
|
235
|
+
await writeFile(temporary, `${JSON.stringify(document, null, 2)}\n`, { encoding: "utf8", mode: 0o600, flag: "wx" });
|
|
236
|
+
await chmod(temporary, 0o600);
|
|
237
|
+
await rename(temporary, path);
|
|
238
|
+
await chmod(path, 0o600).catch(() => undefined);
|
|
239
|
+
} finally {
|
|
240
|
+
await rm(temporary, { force: true }).catch(() => undefined);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function managedSlug(slug: unknown): boolean {
|
|
245
|
+
return typeof slug === "string" && MANAGED_PREFIXES.some(prefix => slug.startsWith(prefix));
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export async function syncCodexCatalog(
|
|
249
|
+
options: CodexCatalogDiscoveryOptions & {
|
|
250
|
+
path?: string;
|
|
251
|
+
discover?: typeof discoverCodexCatalogModels;
|
|
252
|
+
} = {},
|
|
253
|
+
): Promise<CodexCatalogSyncResult> {
|
|
254
|
+
const env = options.env ?? process.env;
|
|
255
|
+
const codexHome = env.CODEX_HOME?.trim() || join(homedir(), ".codex");
|
|
256
|
+
let configuredPath: string | undefined;
|
|
257
|
+
try {
|
|
258
|
+
configuredPath = readRootTomlString(await readFile(join(codexHome, "config.toml"), "utf8"), "model_catalog_json");
|
|
259
|
+
} catch {
|
|
260
|
+
configuredPath = undefined;
|
|
261
|
+
}
|
|
262
|
+
const configured = configuredPath !== undefined;
|
|
263
|
+
const selected = options.path
|
|
264
|
+
? (isAbsolute(options.path) ? options.path : resolve(options.path))
|
|
265
|
+
: configuredPath
|
|
266
|
+
? resolve(codexHome, configuredPath)
|
|
267
|
+
: join(codexHome, "opencodex-catalog.json");
|
|
268
|
+
if (configured && basename(selected) !== "opencodex-catalog.json") {
|
|
269
|
+
return { ok: false, path: selected, reason: "unsafe_path", message: "Configured model_catalog_json is not an opencodex catalog; refusing to overwrite it." };
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
let existing: Record<string, unknown> = {};
|
|
273
|
+
try {
|
|
274
|
+
const parsed = asRecord(JSON.parse(await readFile(selected, "utf8")));
|
|
275
|
+
if (!parsed) return { ok: false, path: selected, reason: "malformed_catalog", message: "Existing Codex catalog is not a JSON object." };
|
|
276
|
+
existing = parsed;
|
|
277
|
+
} catch (error) {
|
|
278
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
|
|
279
|
+
return { ok: false, path: selected, reason: "malformed_catalog", message: "Existing Codex catalog could not be parsed; it was preserved." };
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
let discovered: CodexCatalogModels;
|
|
284
|
+
try {
|
|
285
|
+
discovered = await (options.discover ?? discoverCodexCatalogModels)({ ...options, forceRefresh: true });
|
|
286
|
+
} catch (error) {
|
|
287
|
+
return { ok: false, reason: "discovery_failed", message: error instanceof Error ? error.message : String(error) };
|
|
288
|
+
}
|
|
289
|
+
const existingModels = Array.isArray(existing.models) ? existing.models : [];
|
|
290
|
+
if (existing.models !== undefined && !Array.isArray(existing.models)) {
|
|
291
|
+
return { ok: false, path: selected, reason: "malformed_catalog", message: "Existing Codex catalog models field is not an array; it was preserved." };
|
|
292
|
+
}
|
|
293
|
+
if (existingModels.some(model => !asRecord(model))) {
|
|
294
|
+
return { ok: false, path: selected, reason: "malformed_catalog", message: "Existing Codex catalog contains a non-object model row; it was preserved." };
|
|
295
|
+
}
|
|
296
|
+
const preserved = existingModels.filter(model => !managedSlug(asRecord(model)?.slug));
|
|
297
|
+
const document = buildCodexCatalogDocument(
|
|
298
|
+
discovered.models,
|
|
299
|
+
options.now ?? new Date(),
|
|
300
|
+
preserved as CodexCatalogModel[],
|
|
301
|
+
);
|
|
302
|
+
try {
|
|
303
|
+
await writeCodexCatalog(selected, document);
|
|
304
|
+
} catch (error) {
|
|
305
|
+
return { ok: false, path: selected, reason: "write_failed", message: error instanceof Error ? error.message : String(error) };
|
|
306
|
+
}
|
|
307
|
+
return {
|
|
308
|
+
ok: true,
|
|
309
|
+
path: selected,
|
|
310
|
+
configured,
|
|
311
|
+
written: true,
|
|
312
|
+
modelCount: document.models.length,
|
|
313
|
+
geminiModelCount: document.models.filter(model => model.slug.startsWith("gemini-cli/")).length,
|
|
314
|
+
reasoningModelCount: document.models.filter(model => model.slug.startsWith("gemini-cli/") && (model.supported_reasoning_levels?.length ?? 0) > 0).length,
|
|
315
|
+
reloadRequired: true,
|
|
316
|
+
warnings: discovered.warnings,
|
|
317
|
+
};
|
|
318
|
+
}
|
package/src/lib/redact.ts
CHANGED
|
@@ -2,7 +2,10 @@
|
|
|
2
2
|
export function redactSecretString(value: string): string {
|
|
3
3
|
return value
|
|
4
4
|
.replace(/\bsk-[A-Za-z0-9_-]{6,}\b/g, "[redacted]")
|
|
5
|
-
.replace(
|
|
5
|
+
.replace(/\bAIza[0-9A-Za-z_-]{20,}\b/g, "[redacted]")
|
|
6
|
+
.replace(/\bya29\.[0-9A-Za-z._-]{20,}\b/g, "[redacted]")
|
|
7
|
+
.replace(/(authorization\s*[:=]\s*)(?:bearer\s+)?\S+/gi, "$1[redacted]")
|
|
6
8
|
.replace(/(x-api-key\s*[:=]\s*)\S+/gi, "$1[redacted]")
|
|
9
|
+
.replace(/((?:api[_-]?key|token)\s*[:=]\s*)\S+/gi, "$1[redacted]")
|
|
7
10
|
.slice(0, 4_096);
|
|
8
11
|
}
|
package/src/server/index.ts
CHANGED
|
@@ -9,8 +9,11 @@ import {
|
|
|
9
9
|
} from "../config";
|
|
10
10
|
import type { IncomingMeta } from "../types";
|
|
11
11
|
import type { ClineTurnDeps } from "../adapters/cli-agent/cline/adapter";
|
|
12
|
+
import type { GeminiTurnDeps } from "../adapters/cli-agent/gemini/adapter";
|
|
12
13
|
import { discoverClineLiveModels } from "../adapters/cli-agent/cline/live-models";
|
|
13
|
-
import {
|
|
14
|
+
import { discoverGeminiLiveModels } from "../adapters/cli-agent/gemini/live-models";
|
|
15
|
+
import { buildCodexCatalogDocument, discoverCodexCatalogModels } from "../lib/codex-catalog";
|
|
16
|
+
import { handleResponses, modelList, RequestValidationError, UnsupportedMediaError, type ProviderModels } from "./responses";
|
|
14
17
|
import { handleCountTokens, handleMessages } from "./messages";
|
|
15
18
|
|
|
16
19
|
const MAX_REQUEST_BYTES = 32 * 1024 * 1024;
|
|
@@ -19,6 +22,8 @@ const REQUEST_TIMEOUT_MS = 300_000;
|
|
|
19
22
|
export interface ServerOptions {
|
|
20
23
|
env?: NodeJS.ProcessEnv;
|
|
21
24
|
clineDeps?: ClineTurnDeps;
|
|
25
|
+
geminiDeps?: GeminiTurnDeps;
|
|
26
|
+
geminiPackageRoot?: string;
|
|
22
27
|
}
|
|
23
28
|
|
|
24
29
|
export interface RunningServer {
|
|
@@ -73,11 +78,28 @@ function errorResponse(error: unknown): Response {
|
|
|
73
78
|
return Response.json({ error: { message: "Proxy request failed.", type: "proxy_error", code: "proxy_error" } }, { status: 500 });
|
|
74
79
|
}
|
|
75
80
|
|
|
76
|
-
async function discoverModels(
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
+
async function discoverModels(
|
|
82
|
+
env: NodeJS.ProcessEnv,
|
|
83
|
+
clineDeps: ClineTurnDeps | undefined,
|
|
84
|
+
geminiDeps: GeminiTurnDeps | undefined,
|
|
85
|
+
geminiPackageRoot: string | undefined,
|
|
86
|
+
): Promise<ProviderModels> {
|
|
87
|
+
const [cline, gemini] = await Promise.all([
|
|
88
|
+
discoverClineLiveModels({ env, ...(clineDeps ? { deps: clineDeps } : {}) }),
|
|
89
|
+
discoverGeminiLiveModels({ env, ...(geminiDeps ? { deps: geminiDeps } : {}), ...(geminiPackageRoot ? { packageRoot: geminiPackageRoot } : {}) }),
|
|
90
|
+
]);
|
|
91
|
+
if (!cline.ok && cline.reason !== "not_found" && env.OPENCODEX_CLINE_ACP?.trim() !== "0") {
|
|
92
|
+
console.warn(`[opencodex] Cline model discovery degraded [${cline.reason}]: ${cline.detail}`);
|
|
93
|
+
}
|
|
94
|
+
if (!gemini.ok && gemini.reason !== "not_found" && env.OPENCODEX_GEMINI_ACP?.trim() !== "0") {
|
|
95
|
+
console.warn(`[opencodex] Gemini model discovery degraded [${gemini.reason}]: ${gemini.detail}`);
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
"cline-cli": cline.ok ? ["default", ...cline.models.filter(id => id !== "default")] : ["default"],
|
|
99
|
+
"gemini-cli": gemini.ok
|
|
100
|
+
? ["default", ...gemini.modelInfo.filter(model => model.id !== "default")]
|
|
101
|
+
: ["default"],
|
|
102
|
+
};
|
|
81
103
|
}
|
|
82
104
|
|
|
83
105
|
export function startServer(config: RuntimeConfig | number, options: ServerOptions = {}): RunningServer {
|
|
@@ -86,6 +108,8 @@ export function startServer(config: RuntimeConfig | number, options: ServerOptio
|
|
|
86
108
|
: normalizeConfig(config);
|
|
87
109
|
const env = options.env ?? process.env;
|
|
88
110
|
const clineDeps = { ...options.clineDeps, env: options.clineDeps?.env ?? env };
|
|
111
|
+
const geminiDeps = { ...options.geminiDeps, env: options.geminiDeps?.env ?? env };
|
|
112
|
+
const turnDeps = { cline: clineDeps, gemini: geminiDeps };
|
|
89
113
|
assertSafeListener(config, env);
|
|
90
114
|
const token = apiToken(config, env);
|
|
91
115
|
let server: ReturnType<typeof Bun.serve>;
|
|
@@ -93,28 +117,37 @@ export function startServer(config: RuntimeConfig | number, options: ServerOptio
|
|
|
93
117
|
const fetch = async (req: Request): Promise<Response> => {
|
|
94
118
|
const url = new URL(req.url);
|
|
95
119
|
if (url.pathname === "/healthz") {
|
|
96
|
-
return Response.json({ ok: true, service: "opencodex
|
|
120
|
+
return Response.json({ ok: true, service: "opencodex", version: packageInfo.version });
|
|
97
121
|
}
|
|
98
122
|
if (!authorized(req, token)) {
|
|
99
123
|
return Response.json({ error: { message: "Unauthorized", type: "authentication_error", code: "unauthorized" } }, { status: 401 });
|
|
100
124
|
}
|
|
101
125
|
try {
|
|
102
126
|
if (req.method === "GET" && url.pathname === "/v1/models") {
|
|
103
|
-
const known = await discoverModels(env, options.clineDeps);
|
|
127
|
+
const known = await discoverModels(env, options.clineDeps, options.geminiDeps, options.geminiPackageRoot);
|
|
104
128
|
return Response.json({ object: "list", data: modelList(known) });
|
|
105
129
|
}
|
|
130
|
+
if (req.method === "GET" && url.pathname === "/v1/model-catalog") {
|
|
131
|
+
const discovered = await discoverCodexCatalogModels({
|
|
132
|
+
env,
|
|
133
|
+
...(options.clineDeps ? { clineDeps: options.clineDeps } : {}),
|
|
134
|
+
...(options.geminiDeps ? { geminiDeps: options.geminiDeps } : {}),
|
|
135
|
+
...(options.geminiPackageRoot ? { geminiPackageRoot: options.geminiPackageRoot } : {}),
|
|
136
|
+
});
|
|
137
|
+
return Response.json(buildCodexCatalogDocument(discovered.models));
|
|
138
|
+
}
|
|
106
139
|
if (req.method === "POST" && url.pathname === "/v1/responses") {
|
|
107
140
|
const body = await boundedJson(req);
|
|
108
|
-
const known = await discoverModels(env, options.clineDeps);
|
|
141
|
+
const known = await discoverModels(env, options.clineDeps, options.geminiDeps, options.geminiPackageRoot);
|
|
109
142
|
const signal = AbortSignal.any([req.signal, AbortSignal.timeout(REQUEST_TIMEOUT_MS)]);
|
|
110
143
|
const incoming: IncomingMeta = { headers: req.headers, abortSignal: signal };
|
|
111
|
-
return await handleResponses(body, incoming, known,
|
|
144
|
+
return await handleResponses(body, incoming, known, turnDeps, config.defaultProvider);
|
|
112
145
|
}
|
|
113
146
|
if (req.method === "POST" && url.pathname === "/v1/messages") {
|
|
114
147
|
const body = await boundedJson(req);
|
|
115
|
-
const known = await discoverModels(env, options.clineDeps);
|
|
148
|
+
const known = await discoverModels(env, options.clineDeps, options.geminiDeps, options.geminiPackageRoot);
|
|
116
149
|
const signal = AbortSignal.any([req.signal, AbortSignal.timeout(REQUEST_TIMEOUT_MS)]);
|
|
117
|
-
return await handleMessages(body, { headers: req.headers, abortSignal: signal }, known,
|
|
150
|
+
return await handleMessages(body, { headers: req.headers, abortSignal: signal }, known, turnDeps, config.defaultProvider);
|
|
118
151
|
}
|
|
119
152
|
if (req.method === "POST" && url.pathname === "/v1/messages/count_tokens") {
|
|
120
153
|
return handleCountTokens(await boundedJson(req));
|
|
@@ -125,7 +158,7 @@ export function startServer(config: RuntimeConfig | number, options: ServerOptio
|
|
|
125
158
|
}
|
|
126
159
|
};
|
|
127
160
|
|
|
128
|
-
server = Bun.serve({ hostname: config.host, port: config.port, fetch });
|
|
161
|
+
server = Bun.serve({ hostname: config.host, port: config.port, idleTimeout: 255, fetch });
|
|
129
162
|
return {
|
|
130
163
|
port: server.port ?? config.port,
|
|
131
164
|
host: config.host,
|
package/src/server/messages.ts
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import type { AdapterEvent, IncomingMeta, OcxMessage, OcxParsedRequest, OcxTool, OcxToolChoice } from "../types";
|
|
2
|
-
import {
|
|
2
|
+
import type { ClineTurnDeps } from "../adapters/cli-agent/cline/adapter";
|
|
3
|
+
import { CLINE_PROVIDER_ID, type ProviderId } from "../config";
|
|
3
4
|
import {
|
|
4
5
|
collectResponsesResult,
|
|
5
6
|
RequestValidationError,
|
|
6
|
-
|
|
7
|
+
routedTurn,
|
|
7
8
|
UnsupportedMediaError,
|
|
8
9
|
type DeclaredTool,
|
|
9
10
|
type ParsedResponsesRequest,
|
|
11
|
+
type ProviderModels,
|
|
12
|
+
type ProviderTurnDeps,
|
|
10
13
|
} from "./responses";
|
|
11
14
|
|
|
12
15
|
function record(value: unknown): Record<string, unknown> | undefined {
|
|
@@ -210,14 +213,12 @@ function anthropicUsage(value: unknown): { input_tokens: number; output_tokens:
|
|
|
210
213
|
export async function handleMessages(
|
|
211
214
|
body: unknown,
|
|
212
215
|
incoming: IncomingMeta,
|
|
213
|
-
knownModels: string[],
|
|
214
|
-
deps: ClineTurnDeps = {},
|
|
216
|
+
knownModels: string[] | ProviderModels,
|
|
217
|
+
deps: ClineTurnDeps | ProviderTurnDeps = {},
|
|
218
|
+
defaultProvider: ProviderId = CLINE_PROVIDER_ID,
|
|
215
219
|
): Promise<Response> {
|
|
216
220
|
const parsed = parseMessagesRequest(body);
|
|
217
|
-
parsed.parsed
|
|
218
|
-
? resolveClineModel(parsed.requestedModel, knownModels)
|
|
219
|
-
: "default";
|
|
220
|
-
const run = (emit: (event: AdapterEvent) => void) => runClineTurn(parsed.parsed, incoming, emit, deps);
|
|
221
|
+
const { run } = routedTurn(parsed.parsed, parsed.requestedModel, incoming, knownModels, deps, defaultProvider);
|
|
221
222
|
const result = await collectResponsesResult(parsed, run);
|
|
222
223
|
if (result.failure) {
|
|
223
224
|
return Response.json({
|