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;
|
||||
|
||||
@@ -9,7 +9,7 @@ export interface BackendProfileSpec {
|
||||
transport: "stdio";
|
||||
command: "codex app-server --listen stdio://";
|
||||
status: "registered";
|
||||
requiredSecretKeys: ["auth.json", "config.toml"];
|
||||
requiredSecretKeys: readonly string[];
|
||||
defaultSecretName: string;
|
||||
profileIsolation: "profile-scoped-codex-home";
|
||||
description: string;
|
||||
@@ -59,7 +59,7 @@ const builtinBackendProfileSpecs: readonly BackendProfileSpec[] = [
|
||||
transport: "stdio",
|
||||
command: "codex app-server --listen stdio://",
|
||||
status: "registered",
|
||||
requiredSecretKeys: ["auth.json", "config.toml"],
|
||||
requiredSecretKeys: ["auth.json", "config.toml", "model-catalog.json"],
|
||||
defaultSecretName: "agentrun-v01-provider-dsflash-go",
|
||||
profileIsolation: "profile-scoped-codex-home",
|
||||
description: "DeepSeek V4 Flash profile through OpenCode Zen Go Moon Bridge",
|
||||
@@ -142,7 +142,7 @@ export function backendCapabilities(): JsonRecord[] {
|
||||
return builtinBackendProfileSpecs.map(backendCapability);
|
||||
}
|
||||
|
||||
export function backendCapabilitiesSqlValues(profiles?: readonly BackendProfile[]): string {
|
||||
export function backendCapabilitiesSqlValues(profiles?: readonly BackendProfile[], options: { requiredSecretKeysByProfile?: Record<string, readonly string[]> } = {}): string {
|
||||
const specs = profiles
|
||||
? profiles.map((profile) => {
|
||||
const spec = backendProfileSpec(profile);
|
||||
@@ -151,13 +151,14 @@ export function backendCapabilitiesSqlValues(profiles?: readonly BackendProfile[
|
||||
})
|
||||
: builtinBackendProfileSpecs;
|
||||
return specs.map((spec) => {
|
||||
const requiredSecretKeys = options.requiredSecretKeysByProfile?.[spec.profile] ?? spec.requiredSecretKeys;
|
||||
const capabilities = JSON.stringify({
|
||||
backendKind: spec.backendKind,
|
||||
protocol: spec.protocol,
|
||||
transport: spec.transport,
|
||||
command: spec.command,
|
||||
requiredSecretKeys: spec.requiredSecretKeys,
|
||||
defaultSecretRef: { name: spec.defaultSecretName, keys: spec.requiredSecretKeys },
|
||||
requiredSecretKeys,
|
||||
defaultSecretRef: { name: spec.defaultSecretName, keys: requiredSecretKeys },
|
||||
profileIsolation: spec.profileIsolation,
|
||||
description: spec.description,
|
||||
});
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import type { JsonRecord } from "./types.js";
|
||||
|
||||
export const dsflashGoModelSlug = "deepseek-v4-flash";
|
||||
export const dsflashGoModelCatalogFile = "model-catalog.json";
|
||||
export const dsflashGoRuntimeModelCatalogPath = "/home/agentrun/.codex-dsflash-go/model-catalog.json";
|
||||
|
||||
const dsflashGoModelCatalog: JsonRecord = {
|
||||
models: [
|
||||
{
|
||||
slug: dsflashGoModelSlug,
|
||||
display_name: "DeepSeek V4 Flash via OpenCode Zen Go",
|
||||
description: "DeepSeek V4 Flash exposed to Codex through OpenCode Zen Go and a Responses-compatible Moon Bridge profile.",
|
||||
default_reasoning_level: "xhigh",
|
||||
supported_reasoning_levels: [
|
||||
{ effort: "low", description: "Fast responses with lighter reasoning" },
|
||||
{ effort: "medium", description: "Balanced reasoning" },
|
||||
{ effort: "high", description: "Deep reasoning" },
|
||||
{ effort: "xhigh", description: "Maximum reasoning" },
|
||||
],
|
||||
shell_type: "shell_command",
|
||||
visibility: "list",
|
||||
supported_in_api: true,
|
||||
priority: 0,
|
||||
additional_speed_tiers: [],
|
||||
service_tiers: [],
|
||||
availability_nux: null,
|
||||
upgrade: null,
|
||||
base_instructions: "You are Codex, a coding agent based on DeepSeek V4 Flash. You and the user share one workspace, and your job is to collaborate with them until their goal is genuinely handled.",
|
||||
model_messages: {},
|
||||
supports_reasoning_summaries: true,
|
||||
default_reasoning_summary: "auto",
|
||||
support_verbosity: false,
|
||||
apply_patch_tool_type: "freeform",
|
||||
web_search_tool_type: "text_and_image",
|
||||
truncation_policy: { mode: "tokens", limit: 10000 },
|
||||
supports_parallel_tool_calls: true,
|
||||
supports_image_detail_original: false,
|
||||
context_window: 1_000_000,
|
||||
max_context_window: 1_000_000,
|
||||
auto_compact_token_limit: 900_000,
|
||||
effective_context_window_percent: 95,
|
||||
experimental_supported_tools: [],
|
||||
input_modalities: ["text"],
|
||||
supports_search_tool: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export function dsflashGoModelCatalogJson(): string {
|
||||
return `${JSON.stringify(dsflashGoModelCatalog, null, 2)}\n`;
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ export type FailureKind =
|
||||
| "provider-auth-failed"
|
||||
| "provider-rate-limited"
|
||||
| "provider-invalid-tool-call"
|
||||
| "provider-compact-unsupported"
|
||||
| "provider-unavailable"
|
||||
| "infra-failed"
|
||||
| "session-store-evicted"
|
||||
|
||||
@@ -185,18 +185,16 @@ export function validateExecutionPolicy(record: JsonRecord): ExecutionPolicy {
|
||||
if (typeof timeout !== "number" || !Number.isFinite(timeout) || timeout <= 0) throw new AgentRunError("schema-invalid", "executionPolicy.timeoutMs must be a positive number", { httpStatus: 400 });
|
||||
const secretScope = asRecord(record.secretScope ?? {}, "executionPolicy.secretScope");
|
||||
if (secretScope.allowCredentialEcho !== undefined && secretScope.allowCredentialEcho !== false) throw new AgentRunError("tenant-policy-denied", "allowCredentialEcho must be false", { httpStatus: 403 });
|
||||
const providerCredentials = Array.isArray(secretScope.providerCredentials) ? secretScope.providerCredentials : [];
|
||||
for (const credential of providerCredentials) {
|
||||
const rawProviderCredentials = Array.isArray(secretScope.providerCredentials) ? secretScope.providerCredentials : [];
|
||||
const providerCredentials: NonNullable<ExecutionPolicy["secretScope"]["providerCredentials"]> = [];
|
||||
for (const credential of rawProviderCredentials) {
|
||||
const item = asRecord(credential, "providerCredential");
|
||||
const profile = typeof item.profile === "string" ? item.profile.trim() : "";
|
||||
if (profile.length === 0) throw new AgentRunError("schema-invalid", "provider credential profile is required", { httpStatus: 400 });
|
||||
if (!isBackendProfile(profile)) throw new AgentRunError("schema-invalid", `provider credential profile ${profile} must be a lowercase slug`, { httpStatus: 400, details: { pattern: backendProfilePatternText } });
|
||||
const secretRef = asRecord(item.secretRef, "providerCredential.secretRef");
|
||||
if (typeof secretRef.name !== "string" || secretRef.name.length === 0) throw new AgentRunError("schema-invalid", "provider credential secretRef.name is required", { httpStatus: 400 });
|
||||
const keys = Array.isArray(secretRef.keys) ? secretRef.keys : [];
|
||||
for (const requiredKey of backendProfileSpec(profile)?.requiredSecretKeys ?? []) {
|
||||
if (!keys.includes(requiredKey)) throw new AgentRunError("schema-invalid", `provider credential ${profile} secretRef.keys must include ${requiredKey}`, { httpStatus: 400 });
|
||||
}
|
||||
const secretRef = validateSecretRef(asRecord(item.secretRef, "providerCredential.secretRef"));
|
||||
const keys = [...new Set([...(secretRef.keys ?? []), ...(backendProfileSpec(profile)?.requiredSecretKeys ?? [])])];
|
||||
providerCredentials.push({ profile, secretRef: { ...secretRef, ...(keys.length > 0 ? { keys } : {}) } });
|
||||
}
|
||||
const toolCredentials = validateToolCredentials(secretScope.toolCredentials);
|
||||
const secretScopeResult: ExecutionPolicy["secretScope"] = { allowCredentialEcho: false };
|
||||
|
||||
@@ -162,6 +162,16 @@ SET execution_policy = replace(replace(replace(execution_policy::text,
|
||||
WHERE execution_policy IS NOT NULL AND execution_policy::text LIKE '%ofcx-go%';
|
||||
DELETE FROM agentrun_backends WHERE profile = 'ofcx-go';
|
||||
INSERT INTO agentrun_backends (profile, capabilities, capacity, health, updated_at)
|
||||
VALUES ${backendCapabilitiesSqlValues(["dsflash-go"], { requiredSecretKeysByProfile: { "dsflash-go": ["auth.json", "config.toml"] } })}
|
||||
ON CONFLICT (profile) DO UPDATE SET
|
||||
capabilities = EXCLUDED.capabilities,
|
||||
capacity = EXCLUDED.capacity,
|
||||
health = EXCLUDED.health,
|
||||
updated_at = EXCLUDED.updated_at;
|
||||
`;
|
||||
|
||||
const dsflashGoModelCatalogBackendProfileMigrationSql = `
|
||||
INSERT INTO agentrun_backends (profile, capabilities, capacity, health, updated_at)
|
||||
VALUES ${backendCapabilitiesSqlValues(["dsflash-go"])}
|
||||
ON CONFLICT (profile) DO UPDATE SET
|
||||
capabilities = EXCLUDED.capabilities,
|
||||
@@ -360,6 +370,11 @@ const postgresMigrations: MigrationDefinition[] = [
|
||||
checksum: checksumSql(dsflashGoBackendProfileMigrationSql),
|
||||
sql: dsflashGoBackendProfileMigrationSql,
|
||||
},
|
||||
{
|
||||
id: "009_v01_dsflash_go_model_catalog",
|
||||
checksum: checksumSql(dsflashGoModelCatalogBackendProfileMigrationSql),
|
||||
sql: dsflashGoModelCatalogBackendProfileMigrationSql,
|
||||
},
|
||||
];
|
||||
|
||||
export function postgresMigrationContract(): JsonRecord {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { createHash, randomUUID } from "node:crypto";
|
||||
import { spawn } from "node:child_process";
|
||||
import { AgentRunError } from "../common/errors.js";
|
||||
import { backendProfileSpec, backendProfileSpecs, isBackendProfileSlug } from "../common/backend-profiles.js";
|
||||
import { dsflashGoModelCatalogFile, dsflashGoModelCatalogJson, dsflashGoModelSlug, dsflashGoRuntimeModelCatalogPath } from "../common/model-catalogs.js";
|
||||
import type { AgentRunStore } from "./store.js";
|
||||
import type { BackendProfile, ExecutionPolicy, JsonRecord, JsonValue } from "../common/types.js";
|
||||
import { asRecord, validateBackendProfile } from "../common/validation.js";
|
||||
@@ -33,6 +34,7 @@ interface ProfileConfig {
|
||||
envKey: string;
|
||||
wireApi: "responses";
|
||||
displayName: string;
|
||||
modelCatalogPath?: string;
|
||||
}
|
||||
|
||||
interface RenderedConfig {
|
||||
@@ -110,13 +112,14 @@ export async function setProviderProfileConfig(profileValue: string, body: unkno
|
||||
const profile = validateBackendProfile(profileValue);
|
||||
const spec = requiredSpec(profile);
|
||||
const record = asRecord(body ?? {}, "providerProfileConfig");
|
||||
const configToml = configTomlField(record);
|
||||
const configToml = configTomlField(record, profile);
|
||||
const delegatedBy = delegatedBySummary(record.delegatedBy);
|
||||
const namespace = profileNamespace(options);
|
||||
const existingSecret = await kubectlGetSecret(spec.defaultSecretName, namespace, options.kubectlCommand ?? "kubectl");
|
||||
const existingData = asOptionalRecord(existingSecret?.data);
|
||||
const authJsonData = dataKey(existingData, "auth.json");
|
||||
const credentialHashSuffix = hashDataKey(existingData, "auth.json");
|
||||
const secretData = providerProfileSecretData({ profile, authJsonData, configToml, existingData });
|
||||
const secretManifest: JsonRecord = {
|
||||
apiVersion: "v1",
|
||||
kind: "Secret",
|
||||
@@ -136,14 +139,14 @@ export async function setProviderProfileConfig(profileValue: string, body: unkno
|
||||
},
|
||||
},
|
||||
type: "Opaque",
|
||||
data: providerProfileSecretData({ authJsonData, configToml }),
|
||||
data: secretData,
|
||||
};
|
||||
const applied = await kubectlUpsertSecret(secretManifest, options.kubectlCommand ?? "kubectl");
|
||||
return {
|
||||
action: "provider-profile-config-updated",
|
||||
mutation: true,
|
||||
profile,
|
||||
configured: Boolean(authJsonData),
|
||||
configured: hasRequiredKeys(secretData, spec.requiredSecretKeys),
|
||||
secretRef: secretRefSummary(profile, namespace),
|
||||
resourceVersion: objectPath(applied, ["metadata", "resourceVersion"]),
|
||||
credentialHashSuffix: credentialHashSuffix ?? null,
|
||||
@@ -169,8 +172,11 @@ export async function setProviderProfileCredential(profileValue: string, body: u
|
||||
const record = asRecord(body ?? {}, "providerProfileCredential");
|
||||
const credential = credentialAuthJson(record);
|
||||
const delegatedBy = delegatedBySummary(record.delegatedBy);
|
||||
const renderedConfig = await renderedConfigForWrite(profile, record, options);
|
||||
const namespace = profileNamespace(options);
|
||||
const existingSecret = await kubectlGetSecret(spec.defaultSecretName, namespace, options.kubectlCommand ?? "kubectl");
|
||||
const existingData = asOptionalRecord(existingSecret?.data);
|
||||
const renderedConfig = await renderedConfigForWrite(profile, record, options);
|
||||
const secretData = providerProfileSecretData({ profile, authJsonData: base64Data(credential.authJson), configToml: renderedConfig.configToml, existingData });
|
||||
const secretManifest: JsonRecord = {
|
||||
apiVersion: "v1",
|
||||
kind: "Secret",
|
||||
@@ -190,17 +196,14 @@ export async function setProviderProfileCredential(profileValue: string, body: u
|
||||
},
|
||||
},
|
||||
type: "Opaque",
|
||||
data: {
|
||||
"auth.json": base64Data(credential.authJson),
|
||||
"config.toml": base64Data(renderedConfig.configToml),
|
||||
},
|
||||
data: secretData,
|
||||
};
|
||||
const applied = await kubectlUpsertSecret(secretManifest, options.kubectlCommand ?? "kubectl");
|
||||
return {
|
||||
action: "provider-profile-credential-updated",
|
||||
mutation: true,
|
||||
profile,
|
||||
configured: true,
|
||||
configured: hasRequiredKeys(secretData, spec.requiredSecretKeys),
|
||||
secretRef: secretRefSummary(profile, namespace),
|
||||
resourceVersion: objectPath(applied, ["metadata", "resourceVersion"]),
|
||||
credentialHashSuffix: shortHash(credential.authJson),
|
||||
@@ -357,12 +360,18 @@ function profileFromSecretName(name: string | null): string | null {
|
||||
return profile.length > 0 ? profile : null;
|
||||
}
|
||||
|
||||
function providerProfileSecretData(input: { authJsonData?: string | null; configToml: string }): JsonRecord {
|
||||
function providerProfileSecretData(input: { profile: BackendProfile; authJsonData?: string | null; configToml: string; existingData?: JsonRecord | null }): JsonRecord {
|
||||
const data: JsonRecord = { "config.toml": base64Data(input.configToml) };
|
||||
if (input.authJsonData) data["auth.json"] = input.authJsonData;
|
||||
const modelCatalogData = input.profile === "dsflash-go" ? dataKey(input.existingData ?? null, dsflashGoModelCatalogFile) ?? defaultModelCatalogData(input.profile) : null;
|
||||
if (modelCatalogData) data[dsflashGoModelCatalogFile] = modelCatalogData;
|
||||
return data;
|
||||
}
|
||||
|
||||
function defaultModelCatalogData(profile: BackendProfile): string | null {
|
||||
return profile === "dsflash-go" ? base64Data(dsflashGoModelCatalogJson()) : null;
|
||||
}
|
||||
|
||||
function credentialAuthJson(record: JsonRecord): { authJson: string; source: "auth-json" | "api-key" } {
|
||||
const authJson = record.authJson;
|
||||
if (typeof authJson === "string" && authJson.trim().length > 0) return { authJson: authJsonField(authJson), source: "auth-json" };
|
||||
@@ -397,6 +406,7 @@ function renderConfigToml(config: ProfileConfig): string {
|
||||
"network_access = \"enabled\"",
|
||||
`model_context_window = ${contextWindow}`,
|
||||
`model_auto_compact_token_limit = ${autoCompactTokenLimit}`,
|
||||
...(config.modelCatalogPath ? [`model_catalog_json = ${tomlString(config.modelCatalogPath)}`] : []),
|
||||
"approvals_reviewer = \"user\"",
|
||||
"",
|
||||
`[model_providers.${config.providerName}]`,
|
||||
@@ -439,6 +449,7 @@ function renderedConfigFromProfileConfig(config: ProfileConfig, source: string):
|
||||
baseUrl: config.baseUrl,
|
||||
authField: config.envKey,
|
||||
wireApi: config.wireApi,
|
||||
modelCatalogJson: config.modelCatalogPath ?? null,
|
||||
valuesPrinted: false,
|
||||
},
|
||||
};
|
||||
@@ -461,7 +472,18 @@ async function existingConfigToml(profile: BackendProfile, options: ProviderProf
|
||||
function existingConfigAllowed(profile: BackendProfile, configToml: string): boolean {
|
||||
if (configToml.trim().length === 0) return false;
|
||||
if ((profile === "deepseek" || profile === "dsflash-go") && configToml.includes("hyueapi.com")) return false;
|
||||
if ((profile === "deepseek" || profile === "dsflash-go") && !configToml.includes("hwlab-deepseek-proxy.hwlab-v02.svc.cluster.local")) return false;
|
||||
if (profile === "deepseek" && !configToml.includes("hwlab-deepseek-proxy.hwlab-v02.svc.cluster.local")) return false;
|
||||
if (profile === "dsflash-go") {
|
||||
if (!configToml.includes(dsflashGoModelSlug)) return false;
|
||||
if (modelCatalogPathFromConfigToml(configToml) !== dsflashGoRuntimeModelCatalogPath) return false;
|
||||
const baseUrl = baseUrlFromConfigToml(configToml);
|
||||
if (!baseUrl) return false;
|
||||
try {
|
||||
if (!isAllowedDsflashBridgeUrl(new URL(baseUrl))) return false;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -475,9 +497,10 @@ function configTomlFromData(data: JsonRecord | null, profile: BackendProfile): s
|
||||
}
|
||||
}
|
||||
|
||||
function configTomlField(record: JsonRecord): string {
|
||||
function configTomlField(record: JsonRecord, profile: BackendProfile): string {
|
||||
const value = record.configToml;
|
||||
if (typeof value !== "string" || value.trim().length === 0) throw new AgentRunError("schema-invalid", "configToml is required", { httpStatus: 400 });
|
||||
validateConfigTomlForProfile(profile, value);
|
||||
return value;
|
||||
}
|
||||
|
||||
@@ -489,6 +512,7 @@ function configField(value: unknown, profile: BackendProfile): ProfileConfig {
|
||||
const baseUrl = optionalString(record.baseUrl) ?? defaults.baseUrl;
|
||||
const providerName = optionalString(record.providerName) ?? defaults.providerName;
|
||||
const envKey = optionalString(record.envKey) ?? defaults.envKey;
|
||||
if (profile === "dsflash-go" && model !== dsflashGoModelSlug) throw new AgentRunError("schema-invalid", `dsflash-go model must be ${dsflashGoModelSlug}`, { httpStatus: 400 });
|
||||
validateBaseUrl(profile, baseUrl);
|
||||
if (!/^[A-Z_][A-Z0-9_]{0,63}$/u.test(envKey)) throw new AgentRunError("schema-invalid", "config.envKey must be an uppercase env name", { httpStatus: 400 });
|
||||
if (!/^[A-Za-z][A-Za-z0-9_-]{0,63}$/u.test(providerName)) throw new AgentRunError("schema-invalid", "config.providerName must be a provider identifier", { httpStatus: 400 });
|
||||
@@ -513,7 +537,8 @@ function defaultConfig(profile: BackendProfile): ProfileConfig {
|
||||
baseUrl: "http://hwlab-deepseek-proxy.hwlab-v02.svc.cluster.local:4000/v1",
|
||||
envKey: "OPENAI_API_KEY",
|
||||
wireApi: "responses",
|
||||
displayName: "OpenCode",
|
||||
displayName: "Moon Bridge OpenCode Zen Go Flash",
|
||||
modelCatalogPath: dsflashGoRuntimeModelCatalogPath,
|
||||
};
|
||||
}
|
||||
if (profile === "minimax-m3") {
|
||||
@@ -537,7 +562,7 @@ function defaultConfig(profile: BackendProfile): ProfileConfig {
|
||||
}
|
||||
|
||||
function profileUsesMoonBridge(profile: BackendProfile, configToml: string): boolean {
|
||||
return profile === "deepseek" || profile === "dsflash-go" || configToml.includes("hwlab-deepseek-proxy.hwlab-v02.svc.cluster.local");
|
||||
return profile === "deepseek" || profile === "dsflash-go" || /hwlab-deepseek-proxy\.hwlab-v02\.svc\.cluster\.local|moon|bridge/iu.test(configToml);
|
||||
}
|
||||
|
||||
function validateBaseUrl(profile: BackendProfile, value: string): void {
|
||||
@@ -547,12 +572,50 @@ function validateBaseUrl(profile: BackendProfile, value: string): void {
|
||||
} catch {
|
||||
throw new AgentRunError("schema-invalid", "config.baseUrl must be a valid URL", { httpStatus: 400 });
|
||||
}
|
||||
if ((profile === "deepseek" || profile === "dsflash-go") && url.hostname === "hyueapi.com") {
|
||||
if ((profile === "deepseek" || profile === "dsflash-go") && isHyueHost(url.hostname)) {
|
||||
throw new AgentRunError("tenant-policy-denied", `${profile} profile must use HWLAB Moon Bridge, not hyueapi.com`, { httpStatus: 403 });
|
||||
}
|
||||
if ((profile === "deepseek" || profile === "dsflash-go") && url.hostname !== "hwlab-deepseek-proxy.hwlab-v02.svc.cluster.local") {
|
||||
throw new AgentRunError("tenant-policy-denied", `${profile} profile baseUrl must point to HWLAB v0.2 Moon Bridge`, { httpStatus: 403 });
|
||||
if (profile === "deepseek" && url.hostname !== "hwlab-deepseek-proxy.hwlab-v02.svc.cluster.local") {
|
||||
throw new AgentRunError("tenant-policy-denied", "deepseek profile baseUrl must point to HWLAB v0.2 DeepSeek bridge", { httpStatus: 403 });
|
||||
}
|
||||
if (profile === "dsflash-go" && !isAllowedDsflashBridgeUrl(url)) {
|
||||
throw new AgentRunError("tenant-policy-denied", "dsflash-go profile baseUrl must point to a Moon Bridge service or wrapper-local bridge", { httpStatus: 403 });
|
||||
}
|
||||
}
|
||||
|
||||
function validateConfigTomlForProfile(profile: BackendProfile, configToml: string): void {
|
||||
if ((profile === "deepseek" || profile === "dsflash-go") && /hyueapi\.com/iu.test(configToml)) {
|
||||
throw new AgentRunError("tenant-policy-denied", `${profile} profile must use HWLAB Moon Bridge, not hyueapi.com`, { httpStatus: 403 });
|
||||
}
|
||||
if (profile === "deepseek" && !configToml.includes("hwlab-deepseek-proxy.hwlab-v02.svc.cluster.local")) {
|
||||
throw new AgentRunError("tenant-policy-denied", "deepseek profile config.toml must point to HWLAB v0.2 DeepSeek bridge", { httpStatus: 403 });
|
||||
}
|
||||
if (profile !== "dsflash-go") return;
|
||||
if (!configToml.includes(dsflashGoModelSlug)) throw new AgentRunError("schema-invalid", `dsflash-go config.toml must use model ${dsflashGoModelSlug}`, { httpStatus: 400 });
|
||||
if (modelCatalogPathFromConfigToml(configToml) !== dsflashGoRuntimeModelCatalogPath) throw new AgentRunError("schema-invalid", `dsflash-go config.toml must set model_catalog_json to ${dsflashGoRuntimeModelCatalogPath}`, { httpStatus: 400 });
|
||||
const baseUrl = baseUrlFromConfigToml(configToml);
|
||||
if (!baseUrl) throw new AgentRunError("schema-invalid", "dsflash-go config.toml must include model_providers base_url", { httpStatus: 400 });
|
||||
validateBaseUrl(profile, baseUrl);
|
||||
}
|
||||
|
||||
function isHyueHost(hostname: string): boolean {
|
||||
return hostname === "hyueapi.com" || hostname.endsWith(".hyueapi.com");
|
||||
}
|
||||
|
||||
function isAllowedDsflashBridgeUrl(url: URL): boolean {
|
||||
if (url.hostname === "127.0.0.1" || url.hostname === "localhost") return true;
|
||||
if (url.hostname === "hwlab-deepseek-proxy.hwlab-v02.svc.cluster.local") return true;
|
||||
return url.hostname.endsWith(".svc.cluster.local") && /moon|bridge|deepseek|opencode/iu.test(url.hostname);
|
||||
}
|
||||
|
||||
function baseUrlFromConfigToml(configToml: string): string | null {
|
||||
const match = configToml.match(/^\s*base_url\s*=\s*"([^"]+)"\s*$/mu);
|
||||
return match?.[1] ?? null;
|
||||
}
|
||||
|
||||
function modelCatalogPathFromConfigToml(configToml: string): string | null {
|
||||
const match = configToml.match(/^\s*model_catalog_json\s*=\s*"([^"]+)"\s*$/mu);
|
||||
return match?.[1] ?? null;
|
||||
}
|
||||
|
||||
function contextWindowSettings(model: string): { contextWindow: number; autoCompactTokenLimit: number } {
|
||||
|
||||
@@ -267,13 +267,19 @@ function credentialProjections(run: RunRecord, namespace: string): CredentialPro
|
||||
const credentials = (policy.secretScope.providerCredentials ?? []).filter((item) => item.profile === run.backendProfile);
|
||||
return credentials.map((item, index) => ({
|
||||
profile: item.profile,
|
||||
secretRef: item.secretRef.namespace ? item.secretRef : { ...item.secretRef, namespace },
|
||||
secretRef: credentialSecretRef(item.profile, item.secretRef, namespace),
|
||||
volumeName: sanitizeVolumeName(`${String(item.profile)}-${index}`),
|
||||
runtimeMountPath: normalizeMountPath(item.secretRef.mountPath, String(item.profile)),
|
||||
projectionMountPath: `/var/run/agentrun/secrets/${sanitizeVolumeName(`${String(item.profile)}-${index}`)}`,
|
||||
}));
|
||||
}
|
||||
|
||||
function credentialSecretRef(profile: string, secretRef: SecretRef, namespace: string): SecretRef {
|
||||
const spec = backendProfileSpec(profile);
|
||||
const keys = [...new Set([...(secretRef.keys ?? []), ...(spec?.requiredSecretKeys ?? [])])];
|
||||
return { ...secretRef, namespace: secretRef.namespace ?? namespace, ...(keys.length > 0 ? { keys } : {}) };
|
||||
}
|
||||
|
||||
function toolCredentialProjections(run: RunRecord, namespace: string): ToolCredentialProjection[] {
|
||||
const policy: ExecutionPolicy = run.executionPolicy;
|
||||
const credentials = policy.secretScope.toolCredentials ?? [];
|
||||
|
||||
@@ -13,9 +13,11 @@ const selfTest: SelfTestCase = async () => {
|
||||
(error) => error instanceof AgentRunError && error.failureKind === "infra-failed" && error.message.includes("DATABASE_URL is required"),
|
||||
);
|
||||
const postgresContract = postgresMigrationContract();
|
||||
assert.equal(postgresContract.latestMigrationId, "008_v01_dsflash_go_backend_profile");
|
||||
assert.equal(postgresContract.latestMigrationId, "009_v01_dsflash_go_model_catalog");
|
||||
assert.equal((postgresContract.migrationIds as string[]).includes("008_v01_dsflash_go_backend_profile"), true);
|
||||
assert.equal((postgresContract.migrationIds as string[]).includes("009_v01_dsflash_go_model_catalog"), true);
|
||||
assert.ok(typeof (postgresContract.checksums as Record<string, string>)["008_v01_dsflash_go_backend_profile"] === "string" && (postgresContract.checksums as Record<string, string>)["008_v01_dsflash_go_backend_profile"].length > 0);
|
||||
assert.ok(typeof (postgresContract.checksums as Record<string, string>)["009_v01_dsflash_go_model_catalog"] === "string" && (postgresContract.checksums as Record<string, string>)["009_v01_dsflash_go_model_catalog"].length > 0);
|
||||
assert.equal((postgresContract.checksums as Record<string, string>)["002_v01_backend_profiles"], "928b5c490cc4539cb64ecef34784557601b2724fa2870570f16a53576804e49c");
|
||||
assert.ok(Array.isArray(postgresContract.requiredTables));
|
||||
assert.ok(postgresContract.requiredTables.includes("agentrun_schema_migrations"));
|
||||
|
||||
@@ -96,11 +96,30 @@ const selfTest: SelfTestCase = async (context) => {
|
||||
sourceCommit: "self-test",
|
||||
});
|
||||
assertRunnerJobUsesWritableCodexHome(dsflashGoRendered.manifest as JsonRecord, context.deepseekHome, "dsflash-go-0", "/var/run/agentrun/secrets/dsflash-go-0");
|
||||
assertRunnerJobSecretKeys(dsflashGoRendered, "dsflash-go", ["auth.json", "config.toml", "model-catalog.json"]);
|
||||
assertRunnerJobDoesNotMountProfile(dsflashGoRendered.manifest as JsonRecord, "codex-0");
|
||||
assertRunnerJobDoesNotMountProfile(dsflashGoRendered.manifest as JsonRecord, "deepseek-0");
|
||||
assertRunnerJobDoesNotMountProfile(dsflashGoRendered.manifest as JsonRecord, "minimax-m3-0");
|
||||
assertNoSecretLeak(dsflashGoRendered);
|
||||
|
||||
const legacyDsflashRun = await client.post("/api/v1/runs", {
|
||||
tenantId: "unidesk",
|
||||
projectId: "pikasTech/unidesk",
|
||||
workspaceRef: { kind: "host-path", path: context.workspace },
|
||||
providerId: "G14",
|
||||
backendProfile: "dsflash-go",
|
||||
executionPolicy: {
|
||||
sandbox: "workspace-write",
|
||||
approval: "never",
|
||||
timeoutMs: 15_000,
|
||||
network: "default",
|
||||
secretScope: { allowCredentialEcho: false, providerCredentials: [{ profile: "dsflash-go", secretRef: { name: "agentrun-v01-provider-dsflash-go", keys: ["auth.json", "config.toml"], mountPath: context.deepseekHome } }] },
|
||||
},
|
||||
traceSink: null,
|
||||
}) as RunRecord;
|
||||
const legacyDsflashCredential = legacyDsflashRun.executionPolicy.secretScope.providerCredentials?.[0];
|
||||
assert.deepEqual(legacyDsflashCredential?.secretRef.keys, ["auth.json", "config.toml", "model-catalog.json"]);
|
||||
|
||||
const fakeKubectl = path.join(context.tmp, "fake-kubectl.js");
|
||||
const createdManifest = path.join(context.tmp, "created-runner-job.json");
|
||||
await writeFile(fakeKubectl, `#!/usr/bin/env bun
|
||||
@@ -208,7 +227,7 @@ console.log(JSON.stringify({ apiVersion: manifest.apiVersion, kind: manifest.kin
|
||||
assert.equal(envMap.get("AGENTRUN_SESSION_PVC_NAMESPACE"), "agentrun-v01");
|
||||
assert.equal(envMap.get("AGENTRUN_SESSION_PVC_MOUNT_PATH"), "/home/agentrun/.codex-codex/sessions");
|
||||
assert.equal(envMap.get("AGENTRUN_CODEX_ROLLOUT_SUBDIR"), "sessions");
|
||||
return { name: "runner-k8s-job", tests: ["runner-k8s-job-dry-run", "runner-k8s-job-deepseek-profile-dry-run", "runner-k8s-job-minimax-m3-profile-dry-run", "runner-k8s-job-dsflash-go-profile-dry-run", "runner-k8s-job-create-api", "runner-k8s-job-retention-ttl", "runner-job-transient-env", "runner-job-tool-credential-env", "runner-job-unidesk-ssh-tool-credential-env", "runner-job-unidesk-ssh-transient-env-denied", "runner-k8s-job-session-pvc-volume-and-env"] };
|
||||
return { name: "runner-k8s-job", tests: ["runner-k8s-job-dry-run", "runner-k8s-job-deepseek-profile-dry-run", "runner-k8s-job-minimax-m3-profile-dry-run", "runner-k8s-job-dsflash-go-profile-dry-run", "runner-k8s-job-dsflash-go-legacy-secretref-normalized", "runner-k8s-job-create-api", "runner-k8s-job-retention-ttl", "runner-job-transient-env", "runner-job-tool-credential-env", "runner-job-unidesk-ssh-tool-credential-env", "runner-job-unidesk-ssh-transient-env-denied", "runner-k8s-job-session-pvc-volume-and-env"] };
|
||||
} finally {
|
||||
await new Promise<void>((resolve) => server.server.close(() => resolve()));
|
||||
}
|
||||
@@ -278,6 +297,13 @@ function assertRunnerJobUsesWritableCodexHome(manifest: JsonRecord, expectedCode
|
||||
assert.notEqual(value("CODEX_HOME"), value("AGENTRUN_CODEX_SECRET_HOME"));
|
||||
}
|
||||
|
||||
function assertRunnerJobSecretKeys(rendered: JsonRecord, profile: string, expectedKeys: string[]): void {
|
||||
const refs = rendered.secretRefs as JsonRecord[];
|
||||
const ref = refs.find((item) => item.profile === profile);
|
||||
assert.ok(ref, `${profile} SecretRef summary must be present`);
|
||||
assert.deepEqual(ref.keys, expectedKeys);
|
||||
}
|
||||
|
||||
function assertRunnerJobDoesNotMountProfile(manifest: JsonRecord, volumeName: string): void {
|
||||
const spec = manifest.spec as JsonRecord;
|
||||
const template = spec.template as JsonRecord;
|
||||
|
||||
@@ -62,8 +62,10 @@ const selfTest: SelfTestCase = async (context) => {
|
||||
assert.equal(dsflashGoResult.terminalStatus, "completed");
|
||||
await access(path.join(dsflashGoHome, "auth.json"));
|
||||
await access(path.join(dsflashGoHome, "config.toml"));
|
||||
await access(path.join(dsflashGoHome, "model-catalog.json"));
|
||||
const dsflashGoEvents = await client.get(`/api/v1/runs/${dsflashGo.runId}/events?afterSeq=0&limit=100`) as { items?: Array<{ type: string; payload: unknown }> };
|
||||
assert.ok(dsflashGoEvents.items?.some((event) => event.type === "backend_status" && JSON.stringify(event.payload).includes("dsflash-go")), "dsflash-go backend_status should include profile metadata");
|
||||
assert.ok(dsflashGoEvents.items?.some((event) => event.type === "backend_status" && JSON.stringify(event.payload).includes("\"contextWindow\":1000000")), "dsflash-go backend_status should include 1M context metadata");
|
||||
assertNoSecretLeak(dsflashGoEvents);
|
||||
|
||||
await assert.rejects(
|
||||
@@ -213,6 +215,7 @@ const selfTest: SelfTestCase = async (context) => {
|
||||
await runFailureCase({ client, managerUrl: server.baseUrl, context, mode: "provider-401-rpc-error", expectedStatus: "failed", expectedFailureKind: "provider-auth-failed" });
|
||||
await runFailureCase({ client, managerUrl: server.baseUrl, context, mode: "provider-429-terminal", expectedStatus: "failed", expectedFailureKind: "provider-rate-limited" });
|
||||
await runFailureCase({ client, managerUrl: server.baseUrl, context, mode: "provider-invalid-tool-call", expectedStatus: "failed", expectedFailureKind: "provider-invalid-tool-call" });
|
||||
await runFailureCase({ client, managerUrl: server.baseUrl, context, mode: "provider-compact-404-terminal", expectedStatus: "failed", expectedFailureKind: "provider-compact-unsupported" });
|
||||
await runFailureCase({ client, managerUrl: server.baseUrl, context, mode: "provider-503-rpc-error", expectedStatus: "failed", expectedFailureKind: "provider-unavailable" });
|
||||
await runFailureCase({ client, managerUrl: server.baseUrl, context, mode: "provider-503-terminal", expectedStatus: "failed", expectedFailureKind: "provider-unavailable" });
|
||||
await runFailureCase({ client, managerUrl: server.baseUrl, context, mode: "provider-503-retry-event", expectedStatus: "failed", expectedFailureKind: "provider-unavailable", expectRetryError: true });
|
||||
@@ -227,7 +230,7 @@ const selfTest: SelfTestCase = async (context) => {
|
||||
await runSessionStorageSubdirCase({ client, managerUrl: server.baseUrl, context });
|
||||
await runSessionStorageNoSecretLeakCase({ client, managerUrl: server.baseUrl, context });
|
||||
|
||||
return { name: "codex-stdio", tests: ["runner-lease-heartbeat", "runner-lease-conflict-recovery", "codex-stdio-fake-turn", "codex-stdio-projected-writable-home", "codex-stdio-deepseek-profile-fake-turn", "codex-stdio-dsflash-go-profile-fake-turn", "codex-stdio-minimax-m3-profile-fake-turn", "codex-stdio-deepseek-missing-secret-no-fallback", "codex-stdio-minimax-m3-missing-secret-no-fallback", "codex-stdio-config-model-authoritative", "codex-stdio-explicit-model-forwarded", "codex-stdio-final-agent-message-only", "codex-stdio-web-search-progress", "codex-stdio-stale-thread-resume-failed", "codex-stdio-live-tool-events", "codex-stdio-noisy-reasoning-suppression", "codex-stdio-missing-turn-result", "codex-stdio-provider-auth-failed", "codex-stdio-provider-rate-limited", "codex-stdio-provider-invalid-tool-call", "codex-stdio-provider-503-rpc-error", "codex-stdio-provider-503-terminal", "codex-stdio-provider-503-retry-event", "codex-stdio-invalid-json", "codex-stdio-timeout", "codex-stdio-idle-timeout-progress-refresh", "codex-stdio-command-failure-keeps-run-open", "codex-stdio-secret-unavailable", "codex-stdio-spawn-failure"] };
|
||||
return { name: "codex-stdio", tests: ["runner-lease-heartbeat", "runner-lease-conflict-recovery", "codex-stdio-fake-turn", "codex-stdio-projected-writable-home", "codex-stdio-deepseek-profile-fake-turn", "codex-stdio-dsflash-go-profile-fake-turn", "codex-stdio-dsflash-go-config-metadata", "codex-stdio-minimax-m3-profile-fake-turn", "codex-stdio-deepseek-missing-secret-no-fallback", "codex-stdio-minimax-m3-missing-secret-no-fallback", "codex-stdio-config-model-authoritative", "codex-stdio-explicit-model-forwarded", "codex-stdio-final-agent-message-only", "codex-stdio-web-search-progress", "codex-stdio-stale-thread-resume-failed", "codex-stdio-live-tool-events", "codex-stdio-noisy-reasoning-suppression", "codex-stdio-missing-turn-result", "codex-stdio-provider-auth-failed", "codex-stdio-provider-rate-limited", "codex-stdio-provider-invalid-tool-call", "codex-stdio-provider-compact-unsupported", "codex-stdio-provider-503-rpc-error", "codex-stdio-provider-503-terminal", "codex-stdio-provider-503-retry-event", "codex-stdio-invalid-json", "codex-stdio-timeout", "codex-stdio-idle-timeout-progress-refresh", "codex-stdio-command-failure-keeps-run-open", "codex-stdio-secret-unavailable", "codex-stdio-spawn-failure"] };
|
||||
} finally {
|
||||
await new Promise<void>((resolve) => server.server.close(() => resolve()));
|
||||
}
|
||||
|
||||
@@ -35,8 +35,9 @@ const selfTest: SelfTestCase = async (context) => {
|
||||
const dsflashGoSecretPlan = await renderCodexProviderSecretPlan({ profile: "dsflash-go", codexHome: context.deepseekHome, dryRun: true });
|
||||
assert.equal(dsflashGoSecretPlan.secretName, "agentrun-v01-provider-dsflash-go");
|
||||
assert.equal(dsflashGoSecretPlan.profile, "dsflash-go");
|
||||
assert.deepEqual(dsflashGoSecretPlan.keys, ["auth.json", "config.toml", "model-catalog.json"]);
|
||||
assert.equal(JSON.stringify(dsflashGoSecretPlan).includes("test-token-material-deepseek"), false);
|
||||
assert.equal(JSON.stringify(dsflashGoSecretPlan).includes("deepseek-test"), false);
|
||||
assert.equal(JSON.stringify(dsflashGoSecretPlan).includes("deepseek-v4-flash"), false);
|
||||
|
||||
await assert.rejects(
|
||||
() => renderCodexProviderSecretPlan({ codexHome: path.join(context.tmp, "missing-codex-home"), dryRun: true }),
|
||||
|
||||
@@ -159,7 +159,7 @@ process.exit(1);
|
||||
assert.equal(config.configTomlPrinted, true);
|
||||
assert.equal(JSON.stringify(config).includes("redacted-fixture"), false);
|
||||
|
||||
const updatedConfigToml = "model = \"fixture-updated\"\n";
|
||||
const updatedConfigToml = "model = \"fixture-updated\"\nbase_url = \"http://hwlab-deepseek-proxy.hwlab-v02.svc.cluster.local:4000/v1\"\n";
|
||||
const updatedConfig = await client.put("/api/v1/provider-profiles/deepseek/config", {
|
||||
configToml: updatedConfigToml,
|
||||
delegatedBy: { system: "hwlab-v02", userId: "u1", username: "tester", requestId: "req-config-selftest" },
|
||||
@@ -243,10 +243,14 @@ process.exit(1);
|
||||
const createdData = createdSecretManifest.data as JsonRecord;
|
||||
const createdAuthJson = Buffer.from(String(createdData["auth.json"]), "base64").toString("utf8");
|
||||
const createdConfigToml = Buffer.from(String(createdData["config.toml"]), "base64").toString("utf8");
|
||||
const createdModelCatalog = Buffer.from(String(createdData["model-catalog.json"]), "base64").toString("utf8");
|
||||
assert.equal(createdAuthJson.includes(secretText), true);
|
||||
assert.equal(createdAuthJson.includes("OPENAI_API_KEY"), true);
|
||||
assert.equal(createdConfigToml.includes("deepseek-v4-flash"), true);
|
||||
assert.equal(createdConfigToml.includes("model_catalog_json = \"/home/agentrun/.codex-dsflash-go/model-catalog.json\""), true);
|
||||
assert.equal(createdConfigToml.includes("hwlab-deepseek-proxy.hwlab-v02.svc.cluster.local"), true);
|
||||
assert.equal(JSON.parse(createdModelCatalog).models[0].context_window, 1000000);
|
||||
assert.equal(JSON.parse(createdModelCatalog).models[0].auto_compact_token_limit, 900000);
|
||||
const dsflashShown = await client.get("/api/v1/provider-profiles/dsflash-go") as JsonRecord;
|
||||
assert.equal(dsflashShown.configured, true);
|
||||
assert.equal(dsflashShown.failureKind, null);
|
||||
@@ -341,7 +345,7 @@ process.exit(1);
|
||||
assert.equal(finalValidation.status, "completed");
|
||||
assert.equal(JSON.stringify(finalValidation).includes(secretText), false);
|
||||
assertNoSecretLeak(finalValidation);
|
||||
return { name: "provider-profile-management", tests: ["provider-profiles-list-redacted", "provider-profile-config", "provider-profile-set-key-redacted", "provider-profile-set-auth-json-redacted", "provider-profile-secret-replace-annotation-cleanup", "provider-profile-secret-create-upsert", "provider-profile-config-only-create", "provider-profile-dynamic-slug-roundtrip", "provider-profile-remove-builtin", "provider-profile-remove-dynamic-slug", "provider-profile-deepseek-moon-bridge", "provider-profile-manager-secret-rbac", "provider-profile-validation-runner-job"] };
|
||||
return { name: "provider-profile-management", tests: ["provider-profiles-list-redacted", "provider-profile-config", "provider-profile-set-key-redacted", "provider-profile-set-auth-json-redacted", "provider-profile-secret-replace-annotation-cleanup", "provider-profile-secret-create-upsert", "provider-profile-config-only-create", "provider-profile-dsflash-go-model-catalog", "provider-profile-dynamic-slug-roundtrip", "provider-profile-remove-builtin", "provider-profile-remove-dynamic-slug", "provider-profile-deepseek-moon-bridge", "provider-profile-manager-secret-rbac", "provider-profile-validation-runner-job"] };
|
||||
} finally {
|
||||
await new Promise<void>((resolve) => server.server.close(() => resolve()));
|
||||
}
|
||||
|
||||
@@ -128,6 +128,20 @@ for await (const line of rl) {
|
||||
respond(message.id, { turn });
|
||||
continue;
|
||||
}
|
||||
if (mode === "provider-compact-404-terminal") {
|
||||
turnCounter += 1;
|
||||
const turn = {
|
||||
id: `turn_selftest_${turnCounter}`,
|
||||
status: "failed",
|
||||
error: {
|
||||
message: "Error running remote compact task: unexpected status 404 Not Found: 404 page not found, url: http://hwlab-deepseek-proxy.hwlab-v02.svc.cluster.local:4000/v1/responses/compact",
|
||||
},
|
||||
};
|
||||
notify("turn/started", { turn: { id: turn.id, status: "running" } });
|
||||
notify("turn/completed", { turn });
|
||||
respond(message.id, { turn });
|
||||
continue;
|
||||
}
|
||||
if (mode === "provider-503-retry-event") {
|
||||
turnCounter += 1;
|
||||
const turn = {
|
||||
|
||||
@@ -5,6 +5,7 @@ import assert from "node:assert/strict";
|
||||
import { ManagerClient } from "../mgr/client.js";
|
||||
import type { BackendProfile, JsonRecord } from "../common/types.js";
|
||||
import { backendProfileSpec } from "../common/backend-profiles.js";
|
||||
import { dsflashGoModelCatalogJson } from "../common/model-catalogs.js";
|
||||
|
||||
export interface SelfTestContext {
|
||||
root: string;
|
||||
@@ -41,7 +42,8 @@ export async function createSelfTestContext(root: string): Promise<SelfTestConte
|
||||
await writeFile(path.join(codexHome, "auth.json"), JSON.stringify({ token: "test-token-material" }));
|
||||
await writeFile(path.join(codexHome, "config.toml"), "model = \"gpt-test\"\n");
|
||||
await writeFile(path.join(deepseekHome, "auth.json"), JSON.stringify({ token: "test-token-material-deepseek" }));
|
||||
await writeFile(path.join(deepseekHome, "config.toml"), "model = \"deepseek-test\"\n");
|
||||
await writeFile(path.join(deepseekHome, "config.toml"), "model_provider = \"opencode\"\nmodel = \"deepseek-v4-flash\"\nreview_model = \"deepseek-v4-flash\"\nmodel_context_window = 1000000\nmodel_auto_compact_token_limit = 900000\nmodel_catalog_json = \"model-catalog.json\"\n[model_providers.opencode]\nname = \"OpenCode\"\nbase_url = \"http://hwlab-deepseek-proxy.hwlab-v02.svc.cluster.local:4000/v1\"\nwire_api = \"responses\"\nrequires_openai_auth = true\n");
|
||||
await writeFile(path.join(deepseekHome, "model-catalog.json"), dsflashGoModelCatalogJson());
|
||||
await writeFile(path.join(minimaxM3Home, "auth.json"), JSON.stringify({ token: "test-token-material-minimax-m3" }));
|
||||
await writeFile(path.join(minimaxM3Home, "config.toml"), "model = \"MiniMax-M3\"\nmodel_provider = \"minimax\"\n[model_providers.minimax]\nname = \"MiniMax\"\nbase_url = \"https://api.minimaxi.com/v1\"\nenv_key = \"MINIMAX_API_KEY\"\nwire_api = \"responses\"\n");
|
||||
await writeFile(path.join(workspace, "README.md"), "self-test workspace\n");
|
||||
@@ -93,7 +95,7 @@ function providerCredentials(context: Pick<SelfTestContext, "codexHome"> & Parti
|
||||
profile,
|
||||
secretRef: {
|
||||
name: backendProfileSpec(profile)?.defaultSecretName ?? `agentrun-v01-provider-${profile}`,
|
||||
keys: ["auth.json", "config.toml"],
|
||||
keys: [...(backendProfileSpec(profile)?.requiredSecretKeys ?? ["auth.json", "config.toml"])],
|
||||
mountPath: profileSecretHome(context, profile),
|
||||
},
|
||||
}));
|
||||
|
||||
Reference in New Issue
Block a user