fix: 接入 dsflash-go model catalog

This commit is contained in:
Codex
2026-06-08 23:31:33 +08:00
parent 1bd65a4d1a
commit 6dd8c75528
27 changed files with 485 additions and 123 deletions
+6 -3
View File
@@ -13,7 +13,7 @@ import { renderCodexProviderSecretPlan } from "./secret-render.js";
import type { BackendProfile, CommandRecord, JsonRecord, JsonValue, RunRecord, SessionSummary } from "../../src/common/types.js";
import { AgentRunError, errorToJson } from "../../src/common/errors.js";
import type { RunnerOnceOptions } from "../../src/runner/run-once.js";
import { isBackendProfile } from "../../src/common/backend-profiles.js";
import { backendProfileSpec, isBackendProfile } from "../../src/common/backend-profiles.js";
interface ParsedArgs {
positional: string[];
@@ -387,12 +387,14 @@ async function renderCodexSecret(args: ParsedArgs): Promise<JsonRecord> {
const codexHome = optionalFlag(args, "codex-home");
const authFile = optionalFlag(args, "auth-file");
const configFile = optionalFlag(args, "config-file");
const modelCatalogFile = optionalFlag(args, "model-catalog-file");
const namespace = optionalFlag(args, "namespace");
const secretName = optionalFlag(args, "secret-name");
if (profile) options.profile = profile;
if (codexHome) options.codexHome = codexHome;
if (authFile) options.authFile = authFile;
if (configFile) options.configFile = configFile;
if (modelCatalogFile) options.modelCatalogFile = modelCatalogFile;
if (namespace) options.namespace = namespace;
if (secretName) options.secretName = secretName;
return renderCodexProviderSecretPlan(options);
@@ -707,7 +709,8 @@ function jsonObjectFlag(args: ParsedArgs, name: string): JsonRecord | null {
}
function defaultExecutionPolicy(profile: BackendProfile): JsonRecord {
return { sandbox: "workspace-write", approval: "never", timeoutMs: 900000, network: "enabled", secretScope: { allowCredentialEcho: false, providerCredentials: [{ profile, secretRef: { name: `agentrun-v01-provider-${profile}`, keys: ["auth.json", "config.toml"] } }] } };
const keys = [...(backendProfileSpec(profile)?.requiredSecretKeys ?? ["auth.json", "config.toml"])] as string[];
return { sandbox: "workspace-write", approval: "never", timeoutMs: 900000, network: "enabled", secretScope: { allowCredentialEcho: false, providerCredentials: [{ profile, secretRef: { name: `agentrun-v01-provider-${profile}`, keys } }] } };
}
function copyOptionalFlag(args: ParsedArgs, target: JsonRecord, flagName: string, key = flagName.replace(/-([a-z])/gu, (_, letter: string) => letter.toUpperCase())): void {
@@ -792,7 +795,7 @@ function help(): JsonRecord {
"queue cancel <taskId> [--reason <text>]",
"queue dispatch <taskId> [--json-file <dispatch.json>] [--idempotency-key <key>] [--image <image>] [--namespace <namespace>]",
"queue refresh <taskId>",
"secrets codex render --dry-run [--profile codex|deepseek|minimax-m3|dsflash-go] [--codex-home <dir>] [--namespace agentrun-v01] [--secret-name <name>]",
"secrets codex render --dry-run [--profile codex|deepseek|minimax-m3|dsflash-go] [--codex-home <dir>] [--model-catalog-file <file>] [--namespace agentrun-v01] [--secret-name <name>]",
"provider-profiles list",
"provider-profiles show <profile>",
"provider-profiles config <profile>",
+43 -8
View File
@@ -12,13 +12,14 @@ export interface CodexSecretRenderOptions {
codexHome?: string;
authFile?: string;
configFile?: string;
modelCatalogFile?: string;
namespace?: string;
secretName?: string;
dryRun?: boolean;
}
interface SecretFileSummary extends JsonRecord {
key: "auth.json" | "config.toml";
key: string;
source: string;
bytes: number;
sha256: string;
@@ -26,13 +27,12 @@ interface SecretFileSummary extends JsonRecord {
}
interface SecretSourceFile {
key: "auth.json" | "config.toml";
key: string;
path: string;
validate: (content: string, file: string) => unknown;
}
const defaultNamespace = "agentrun-v01";
const secretKeys = ["auth.json", "config.toml"] as const;
const credentialKeyPattern = /(?:api[_-]?key|token|password|secret|credential|authorization|auth)/iu;
export async function renderCodexProviderSecretPlan(options: CodexSecretRenderOptions = {}): Promise<JsonRecord> {
@@ -46,10 +46,8 @@ export async function renderCodexProviderSecretPlan(options: CodexSecretRenderOp
const namespace = nonEmpty(options.namespace, defaultNamespace);
const secretName = nonEmpty(options.secretName, spec?.defaultSecretName ?? "agentrun-v01-provider-codex");
const codexHome = resolvePath(nonEmpty(options.codexHome, path.join(os.homedir(), ".codex")));
const sources: SecretSourceFile[] = [
{ key: "auth.json", path: resolvePath(options.authFile ?? path.join(codexHome, "auth.json")), validate: validateAuthJson },
{ key: "config.toml", path: resolvePath(options.configFile ?? path.join(codexHome, "config.toml")), validate: validateConfigToml },
];
const secretKeys = [...(spec?.requiredSecretKeys ?? ["auth.json", "config.toml"])] as string[];
const sources = secretKeys.map((key): SecretSourceFile => sourceForSecretKey(key, codexHome, options));
const files: SecretFileSummary[] = [];
const hash = createHash("sha256");
@@ -89,7 +87,7 @@ export async function renderCodexProviderSecretPlan(options: CodexSecretRenderOp
manifestSummary,
apply: {
attempted: false,
command: `kubectl create secret generic ${secretName} -n ${namespace} --from-file=auth.json=<redacted> --from-file=config.toml=<redacted> --dry-run=client -o yaml | kubectl apply -f -`,
command: `kubectl create secret generic ${secretName} -n ${namespace} ${secretKeys.map((key) => `--from-file=${key}=<redacted>`).join(" ")} --dry-run=client -o yaml | kubectl apply -f -`,
note: "本命令只展示形状;v0.1 工具不会执行 kubectl apply,也不会输出 Secret data。",
},
redaction: {
@@ -101,6 +99,13 @@ export async function renderCodexProviderSecretPlan(options: CodexSecretRenderOp
};
}
function sourceForSecretKey(key: string, codexHome: string, options: CodexSecretRenderOptions): SecretSourceFile {
if (key === "auth.json") return { key, path: resolvePath(options.authFile ?? path.join(codexHome, key)), validate: validateAuthJson };
if (key === "config.toml") return { key, path: resolvePath(options.configFile ?? path.join(codexHome, key)), validate: validateConfigToml };
if (key === "model-catalog.json") return { key, path: resolvePath(options.modelCatalogFile ?? path.join(codexHome, key)), validate: validateModelCatalogJson };
return { key, path: resolvePath(path.join(codexHome, key)), validate: validateNonEmptyFile(key) };
}
async function readSecretInput(source: SecretSourceFile): Promise<string> {
try {
await access(source.path, fsConstants.R_OK);
@@ -154,6 +159,36 @@ function validateConfigToml(content: string, file: string): unknown {
return parsed;
}
function validateModelCatalogJson(content: string, file: string): unknown {
let parsed: unknown;
try {
parsed = JSON.parse(content);
} catch {
throw new AgentRunError("schema-invalid", "model-catalog.json is not valid JSON", { httpStatus: 2, details: { key: "model-catalog.json", path: file } });
}
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
throw new AgentRunError("schema-invalid", "model-catalog.json must contain a JSON object", { httpStatus: 2, details: { key: "model-catalog.json", path: file } });
}
const models = (parsed as { models?: unknown }).models;
if (!Array.isArray(models) || models.length === 0) {
throw new AgentRunError("schema-invalid", "model-catalog.json must contain a non-empty models array", { httpStatus: 2, details: { key: "model-catalog.json", path: file } });
}
for (const [index, model] of models.entries()) {
const item = typeof model === "object" && model !== null && !Array.isArray(model) ? model as JsonRecord : null;
if (!item) throw new AgentRunError("schema-invalid", `model-catalog.json models[${index}] must be an object`, { httpStatus: 2, details: { key: "model-catalog.json", path: file } });
if (typeof item.slug !== "string" || item.slug.trim().length === 0) throw new AgentRunError("schema-invalid", `model-catalog.json models[${index}].slug is required`, { httpStatus: 2, details: { key: "model-catalog.json", path: file } });
if (typeof item.context_window !== "number" || !Number.isFinite(item.context_window) || item.context_window <= 0) throw new AgentRunError("schema-invalid", `model-catalog.json models[${index}].context_window must be a positive number`, { httpStatus: 2, details: { key: "model-catalog.json", path: file } });
}
return parsed;
}
function validateNonEmptyFile(key: string): (content: string, file: string) => unknown {
return (content: string, file: string) => {
if (content.length === 0) throw new AgentRunError("secret-unavailable", `${key} is empty`, { httpStatus: 2, details: { key, path: file } });
return content;
};
}
function hasNonEmptyCredentialField(value: unknown): boolean {
if (Array.isArray(value)) return value.some((item) => hasNonEmptyCredentialField(item));
if (typeof value !== "object" || value === null) return false;