fix: 接入 dsflash-go model catalog
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process";
|
||||
import { createHash } from "node:crypto";
|
||||
import { accessSync, constants as fsConstants } from "node:fs";
|
||||
import { accessSync, constants as fsConstants, readdirSync, readFileSync } from "node:fs";
|
||||
import { chmod, copyFile, mkdir } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import * as readline from "node:readline";
|
||||
@@ -351,6 +351,7 @@ export class CodexStdioBackendSession {
|
||||
...backendMetadata(options),
|
||||
protocol: codexProtocol,
|
||||
runtime: runtimeSummary(options, env, resolveCodexHome(options)),
|
||||
config: codexConfigSummary(resolveCodexHome(options), options.backendProfile ?? "codex"),
|
||||
},
|
||||
});
|
||||
const clientOptions: ConstructorParameters<typeof CodexStdioClient>[0] = {
|
||||
@@ -386,7 +387,7 @@ async function runCodexStdioTurnWithSession(options: CodexStdioTurnOptions, sess
|
||||
const codexHome = resolveCodexHome(options);
|
||||
const projectionFailure = await prepareProjectedCodexHome(codexHome, options.env?.AGENTRUN_CODEX_SECRET_HOME ?? process.env.AGENTRUN_CODEX_SECRET_HOME);
|
||||
if (projectionFailure) return projectionFailure;
|
||||
const secretFailure = codexHomeReadiness(codexHome);
|
||||
const secretFailure = codexHomeReadiness(codexHome, options.backendProfile ?? "codex");
|
||||
if (secretFailure) return secretFailure;
|
||||
const env = childEnv(options, codexHome);
|
||||
const events: BackendEvent[] = [];
|
||||
@@ -561,7 +562,7 @@ async function prepareProjectedCodexHome(codexHome: string, projectedHome: strin
|
||||
if (path.resolve(projectedHome) === path.resolve(codexHome)) return null;
|
||||
try {
|
||||
await mkdir(codexHome, { recursive: true, mode: 0o700 });
|
||||
for (const fileName of ["auth.json", "config.toml"]) {
|
||||
for (const fileName of projectedCodexHomeFiles(projectedHome)) {
|
||||
await copyFile(path.join(projectedHome, fileName), path.join(codexHome, fileName));
|
||||
await chmod(path.join(codexHome, fileName), 0o600);
|
||||
}
|
||||
@@ -588,16 +589,18 @@ async function prepareProjectedCodexHome(codexHome: string, projectedHome: strin
|
||||
}
|
||||
}
|
||||
|
||||
function codexHomeReadiness(codexHome: string): BackendTurnResult | null {
|
||||
function codexHomeReadiness(codexHome: string, profile: BackendProfile): BackendTurnResult | null {
|
||||
const auth = fileReadable(`${codexHome}/auth.json`);
|
||||
const config = fileReadable(`${codexHome}/config.toml`);
|
||||
if (auth.readable && config.readable) return null;
|
||||
const modelCatalog = auth.readable && config.readable ? modelCatalogReadiness(codexHome, profile) : null;
|
||||
if (auth.readable && config.readable && !modelCatalog) return null;
|
||||
const payload = {
|
||||
failureKind: "secret-unavailable",
|
||||
projection: {
|
||||
codexHome: pathSummary(codexHome),
|
||||
authJson: auth,
|
||||
configToml: config,
|
||||
...(modelCatalog ? { modelCatalogJson: modelCatalog } : {}),
|
||||
valuesPrinted: false,
|
||||
},
|
||||
} satisfies JsonRecord;
|
||||
@@ -612,6 +615,88 @@ function codexHomeReadiness(codexHome: string): BackendTurnResult | null {
|
||||
};
|
||||
}
|
||||
|
||||
function projectedCodexHomeFiles(projectedHome: string): string[] {
|
||||
const required = ["auth.json", "config.toml"];
|
||||
try {
|
||||
const optionalFiles = readdirSync(projectedHome).filter((name) => name === "model-catalog.json");
|
||||
return [...required, ...optionalFiles];
|
||||
} catch {
|
||||
return required;
|
||||
}
|
||||
}
|
||||
|
||||
function modelCatalogReadiness(codexHome: string, profile: BackendProfile): JsonRecord | null {
|
||||
let configToml = "";
|
||||
try {
|
||||
configToml = readFileSync(path.join(codexHome, "config.toml"), "utf8");
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
const modelCatalogPath = modelCatalogJsonPathFromConfig(configToml, codexHome);
|
||||
if (!modelCatalogPath) {
|
||||
return profile === "dsflash-go" ? { present: false, requiredByConfig: false, requiredByProfile: true, valuesPrinted: false } : null;
|
||||
}
|
||||
const readiness = fileReadable(modelCatalogPath);
|
||||
return readiness.readable ? null : { ...readiness, requiredByConfig: true, valuesPrinted: false };
|
||||
}
|
||||
|
||||
function modelCatalogJsonPathFromConfig(configToml: string, codexHome: string): string | null {
|
||||
const match = configToml.match(/^\s*model_catalog_json\s*=\s*"([^"]+)"\s*$/mu);
|
||||
if (!match?.[1]) return null;
|
||||
return path.isAbsolute(match[1]) ? match[1] : path.join(codexHome, match[1]);
|
||||
}
|
||||
|
||||
function codexConfigSummary(codexHome: string, profile: BackendProfile): JsonRecord {
|
||||
const configPath = path.join(codexHome, "config.toml");
|
||||
let configToml = "";
|
||||
try {
|
||||
configToml = readFileSync(configPath, "utf8");
|
||||
} catch {
|
||||
return { available: false, valuesPrinted: false };
|
||||
}
|
||||
const model = tomlStringValue(configToml, "model");
|
||||
const providerName = tomlStringValue(configToml, "model_provider");
|
||||
const baseUrl = tomlStringValue(configToml, "base_url");
|
||||
const modelCatalogPath = modelCatalogJsonPathFromConfig(configToml, codexHome);
|
||||
return {
|
||||
available: true,
|
||||
profile,
|
||||
model,
|
||||
providerName,
|
||||
baseUrl: baseUrl ? redactedUrlSummary(baseUrl) : null,
|
||||
wireApi: tomlStringValue(configToml, "wire_api"),
|
||||
contextWindow: tomlNumberValue(configToml, "model_context_window"),
|
||||
autoCompactTokenLimit: tomlNumberValue(configToml, "model_auto_compact_token_limit"),
|
||||
modelCatalogJson: modelCatalogPath ? { ...pathSummary(modelCatalogPath), readable: fileReadable(modelCatalogPath).readable } : null,
|
||||
valuesPrinted: false,
|
||||
};
|
||||
}
|
||||
|
||||
function tomlStringValue(configToml: string, key: string): string | null {
|
||||
const match = configToml.match(new RegExp(`^\\s*${escapeRegExp(key)}\\s*=\\s*"([^"]*)"\\s*$`, "mu"));
|
||||
return match?.[1] ?? null;
|
||||
}
|
||||
|
||||
function tomlNumberValue(configToml: string, key: string): number | null {
|
||||
const match = configToml.match(new RegExp(`^\\s*${escapeRegExp(key)}\\s*=\\s*([0-9][0-9_]*)\\s*$`, "mu"));
|
||||
if (!match?.[1]) return null;
|
||||
const value = Number(match[1].replace(/_/gu, ""));
|
||||
return Number.isFinite(value) ? value : null;
|
||||
}
|
||||
|
||||
function redactedUrlSummary(value: string): JsonRecord {
|
||||
try {
|
||||
const url = new URL(value);
|
||||
return { protocol: url.protocol.replace(/:$/u, ""), hostname: url.hostname, port: url.port || null, pathname: url.pathname, valuesPrinted: false };
|
||||
} catch {
|
||||
return { valid: false, fingerprint: shortHash(value), valuesPrinted: false };
|
||||
}
|
||||
}
|
||||
|
||||
function escapeRegExp(value: string): string {
|
||||
return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
||||
}
|
||||
|
||||
function normalizeCodexNotification(message: JsonRecord, suppressed: SuppressedNotificationSummary): { events: BackendEvent[]; assistantDelta?: { itemId: string | null; text: string }; completedAssistantMessage?: CompletedAssistantMessage; threadId?: string; turnId?: string; terminal?: { status: TerminalStatus; failureKind: FailureKind | null; message: string | null } } {
|
||||
const method = typeof message.method === "string" ? message.method : "unknown";
|
||||
const params = asRecordAt(message, "params");
|
||||
@@ -1063,6 +1148,7 @@ function classifyCodexErrorRecord(error: JsonRecord, fallback: FailureKind): Fai
|
||||
|
||||
function classifyMessageFailureKind(message: string, fallback: FailureKind): FailureKind {
|
||||
const text = String(message || "").toLowerCase();
|
||||
if (isProviderCompactUnsupportedMessage(text)) return "provider-compact-unsupported";
|
||||
if (/invalid[_ -]?prompt/u.test(text) && /invalid function arguments json string|tool_call_id/u.test(text)) return "provider-invalid-tool-call";
|
||||
if (/invalid function arguments json string/u.test(text)) return "provider-invalid-tool-call";
|
||||
if (/rate.?limit|too many requests|\b429\b/u.test(text)) return "provider-rate-limited";
|
||||
@@ -1073,6 +1159,10 @@ function classifyMessageFailureKind(message: string, fallback: FailureKind): Fai
|
||||
return fallback;
|
||||
}
|
||||
|
||||
function isProviderCompactUnsupportedMessage(text: string): boolean {
|
||||
return /responses\/compact|\/compact\b/u.test(text) && /\b404\b|not found|unsupported|no route|not implemented/u.test(text);
|
||||
}
|
||||
|
||||
function isProviderUnavailableMessage(text: string): boolean {
|
||||
if (/\b(?:http(?:\s+status)?|status(?:\s+code)?|code)\s*[:=]?\s*5\d\d\b/u.test(text)) return true;
|
||||
if (/\b5\d\d\b/u.test(text) && /service unavailable|bad gateway|gateway timeout|internal server error|provider|upstream|response\s*stream\s*disconnected|responsestreamdisconnected/u.test(text)) return true;
|
||||
|
||||
Reference in New Issue
Block a user