|
|
@@ -1,6 +1,7 @@
|
|
|
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
|
import { join } from 'node:path';
|
|
|
import { getCodexDataDir } from './codexHome';
|
|
|
+import { DEFAULT_MODEL_INSTRUCTIONS } from './defaultModelInstructions';
|
|
|
import { DEFAULT_API_KEY_ENV, PROVIDER_EXTRA_ALLOWLIST, type CodexProviderSpec, type CodexRuntime, type JsonValue } from './codexRuntime';
|
|
|
import type { AppliedProviderInfo, ApplyProviderInput } from './types';
|
|
|
|
|
|
@@ -99,6 +100,10 @@ function readStringMap(raw: unknown): Record<string, string> | null {
|
|
|
* 后端「模型管理」的一条记录一律翻译成自定义 provider:Codex 0.155.1 把 ollama / lmstudio
|
|
|
* 等内置 id 列为保留字,覆盖 model_providers.<内置 id> 会让 app-server 启动即退出。
|
|
|
* 地址缺失就是配置缺失,直接报错让页面提示,不做 localhost 兜底。
|
|
|
+ *
|
|
|
+ * model 目录(model_catalog_json)在 applyProvider 里另行生成落盘后挂到 spec 上:
|
|
|
+ * 自定义模型 slug 不在 codex 内置目录里,线程会落到兜底元数据并打出
|
|
|
+ * "Model metadata for `xxx` not found" 警告(上下文窗口/截断策略全靠猜)。
|
|
|
*/
|
|
|
export function toProviderSpec(input: ApplyProviderInput): CodexProviderSpec {
|
|
|
const model = `${input.modelId ?? ''}`.trim();
|
|
|
@@ -264,6 +269,64 @@ async function writeAppliedProvider(value: AppliedProvider | null): Promise<void
|
|
|
await writeFile(providerFile(), JSON.stringify(value ?? {}, null, 2), 'utf8');
|
|
|
}
|
|
|
|
|
|
+/**
|
|
|
+ * 生成单条模型的目录条目,字段与 codex rust-v0.155.1 的 ModelInfo serde 形状对齐
|
|
|
+ * (实测 0.155.1 会校验:catalog 模型必须携带 base_instructions/instructions_template,
|
|
|
+ * 未知字段会报错,所以只写确认过的键)。元数据取 codex 对未知模型的兜底值,
|
|
|
+ * 这样除 used_fallback 归零(警告消失、特性门控放行)外,请求行为与之前完全一致。
|
|
|
+ */
|
|
|
+function buildModelCatalog(spec: CodexProviderSpec): Record<string, unknown> {
|
|
|
+ return {
|
|
|
+ models: [
|
|
|
+ {
|
|
|
+ slug: spec.model,
|
|
|
+ display_name: spec.name ?? spec.model,
|
|
|
+ description: null,
|
|
|
+ default_reasoning_level: null,
|
|
|
+ supported_reasoning_levels: [],
|
|
|
+ shell_type: 'shell_command',
|
|
|
+ visibility: 'list',
|
|
|
+ supported_in_api: true,
|
|
|
+ priority: 1000,
|
|
|
+ additional_speed_tiers: [],
|
|
|
+ service_tiers: [],
|
|
|
+ default_service_tier: null,
|
|
|
+ availability_nux: null,
|
|
|
+ upgrade: null,
|
|
|
+ default_reasoning_summary: 'none',
|
|
|
+ support_verbosity: false,
|
|
|
+ default_verbosity: null,
|
|
|
+ supports_image_detail_original: false,
|
|
|
+ supports_parallel_tool_calls: false,
|
|
|
+ supports_reasoning_summaries: true,
|
|
|
+ supports_search_tool: false,
|
|
|
+ truncation_policy: { mode: 'bytes', limit: 10_000 },
|
|
|
+ context_window: 272_000,
|
|
|
+ max_context_window: 272_000,
|
|
|
+ effective_context_window_percent: 95,
|
|
|
+ experimental_supported_tools: [],
|
|
|
+ input_modalities: ['text'],
|
|
|
+ model_messages: { instructions_template: DEFAULT_MODEL_INSTRUCTIONS },
|
|
|
+ },
|
|
|
+ ],
|
|
|
+ };
|
|
|
+}
|
|
|
+
|
|
|
+function catalogFile(): string {
|
|
|
+ return join(getCodexDataDir(), 'model-catalog.json');
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 把目录文件落盘并返回路径。目录整体替换 codex 的内置目录(进程级),
|
|
|
+ * 单条 provider 对应单条模型;换模型会走 applyProvider 重启子进程,天然重建。
|
|
|
+ */
|
|
|
+async function writeModelCatalog(spec: CodexProviderSpec): Promise<string> {
|
|
|
+ const path = catalogFile();
|
|
|
+ await mkdir(getCodexDataDir(), { recursive: true });
|
|
|
+ await writeFile(path, JSON.stringify(buildModelCatalog(spec), null, 2), 'utf8');
|
|
|
+ return path;
|
|
|
+}
|
|
|
+
|
|
|
/** 应用一条模型配置:先探端点,再重启子进程(api_key 走 env,换 key 必须重启) */
|
|
|
export async function applyProvider(
|
|
|
runtime: CodexRuntime,
|
|
|
@@ -279,6 +342,7 @@ export async function applyProvider(
|
|
|
});
|
|
|
if (probe.protocol !== 'responses') throw new Error(probe.detail);
|
|
|
|
|
|
+ spec.modelCatalogPath = await writeModelCatalog(spec);
|
|
|
await runtime.applyProvider(spec);
|
|
|
|
|
|
const applied: AppliedProvider = {
|