feat: add code agent provider profiles

This commit is contained in:
Codex
2026-05-26 01:47:11 +08:00
parent c0d1dac33d
commit 83cf496fcd
11 changed files with 469 additions and 136 deletions
+171 -32
View File
@@ -15,12 +15,19 @@ const defaultBranch = process.env.HWLAB_G14_BRANCH || "G14";
const defaultGitopsBranch = process.env.HWLAB_G14_GITOPS_BRANCH || "G14-gitops";
const defaultRuntimeEndpoint = process.env.HWLAB_G14_RUNTIME_ENDPOINT || "http://74.48.78.17:17667";
const defaultWebEndpoint = process.env.HWLAB_G14_WEB_ENDPOINT || "http://74.48.78.17:17666";
const defaultProdRuntimeEndpoint = process.env.HWLAB_G14_PROD_RUNTIME_ENDPOINT || "http://74.48.78.17:18667";
const defaultProdWebEndpoint = process.env.HWLAB_G14_PROD_WEB_ENDPOINT || "http://74.48.78.17:18666";
const defaultProxyUrl = process.env.HWLAB_G14_PROXY_URL || "http://127.0.0.1:10808";
const defaultAllProxyUrl = process.env.HWLAB_G14_ALL_PROXY_URL || "socks5h://127.0.0.1:10808";
const defaultNoProxy = process.env.HWLAB_G14_NO_PROXY || "127.0.0.1,localhost,::1,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,192.168.0.0/16,.svc,.cluster.local,kubernetes.default.svc,registry.hwlab-ci.svc,hwlab-registry.hwlab-ci.svc";
const ciJsonRunnerImage = process.env.HWLAB_G14_CI_JSON_IMAGE || "mcr.microsoft.com/playwright@sha256:b0ab6f3cb99aa7803adbc14d9027ec1785fc6e433b97e134e0f8fe61683b6b53";
const ciToolsRunnerImage = process.env.HWLAB_G14_CI_TOOLS_IMAGE || "127.0.0.1:5000/hwlab/hwlab-ci-node-tools:node22-alpine-v1";
const defaultDevBaseImage = process.env.HWLAB_G14_DEV_BASE_IMAGE || "127.0.0.1:5000/hwlab/hwlab-node20-base:20-bookworm-slim";
const deepSeekProxyImage = process.env.HWLAB_G14_DEEPSEEK_PROXY_IMAGE || "ghcr.io/berriai/litellm:main-latest";
const deepSeekLiteLlmModel = process.env.HWLAB_G14_DEEPSEEK_LITELLM_MODEL || "deepseek/deepseek-chat";
const deepSeekProfileModel = process.env.HWLAB_G14_DEEPSEEK_PROFILE_MODEL || "deepseek-chat";
const codexApiProfileModel = process.env.HWLAB_G14_CODEX_API_PROFILE_MODEL || "gpt-5.5";
const codexApiProfileBaseUrl = process.env.HWLAB_G14_CODEX_API_PROFILE_BASE_URL || "http://172.26.26.227:17680/v1/responses";
const sourceCommitPattern = /^[a-f0-9]{7,40}$/u;
function proxyEnv() {
@@ -46,6 +53,8 @@ function parseArgs(argv) {
sourceRepo: defaultSourceRepo,
runtimeEndpoint: defaultRuntimeEndpoint,
webEndpoint: defaultWebEndpoint,
prodRuntimeEndpoint: defaultProdRuntimeEndpoint,
prodWebEndpoint: defaultProdWebEndpoint,
check: false,
write: true,
help: false
@@ -60,6 +69,8 @@ function parseArgs(argv) {
else if (arg === "--source-repo") args.sourceRepo = readOption(argv, ++index, arg);
else if (arg === "--runtime-endpoint") args.runtimeEndpoint = readOption(argv, ++index, arg);
else if (arg === "--web-endpoint") args.webEndpoint = readOption(argv, ++index, arg);
else if (arg === "--prod-runtime-endpoint") args.prodRuntimeEndpoint = readOption(argv, ++index, arg);
else if (arg === "--prod-web-endpoint") args.prodWebEndpoint = readOption(argv, ++index, arg);
else if (arg === "--check") {
args.check = true;
args.write = false;
@@ -91,6 +102,8 @@ function usage() {
` --registry-prefix PREFIX default: ${defaultRegistryPrefix}`,
` --runtime-endpoint URL default: ${defaultRuntimeEndpoint}`,
` --web-endpoint URL default: ${defaultWebEndpoint}`,
` --prod-runtime-endpoint URL default: ${defaultProdRuntimeEndpoint}`,
` --prod-web-endpoint URL default: ${defaultProdWebEndpoint}`,
" --check verify generated files are current without writing"
].join("\n");
}
@@ -181,24 +194,53 @@ function imageFor(registryPrefix, serviceId, shortCommit) {
return `${registryPrefix}/${serviceId}:${shortCommit}`;
}
function transformWorkloads({ workloads, deploy, source, registryPrefix, runtimeEndpoint, webEndpoint }) {
function namespaceNameForProfile(profile) {
return profile === "prod" ? "hwlab-prod" : "hwlab-dev";
}
function runtimePathForProfile(profile) {
return profile === "prod" ? "runtime-prod" : "runtime-dev";
}
function runtimeLabelForProfile(profile) {
return profile === "prod" ? "prod" : "dev";
}
function profileEndpoint(args, profile) {
return profile === "prod"
? { runtimeEndpoint: args.prodRuntimeEndpoint, webEndpoint: args.prodWebEndpoint }
: { runtimeEndpoint: args.runtimeEndpoint, webEndpoint: args.webEndpoint };
}
function deepSeekBaseUrl(namespace) {
return `http://hwlab-deepseek-proxy.${namespace}.svc.cluster.local:4000/v1/responses`;
}
function namespaceScopedHost(value, namespace) {
if (typeof value !== "string") return value;
return value.replace(/\.hwlab-dev\.svc\.cluster\.local/gu, `.${namespace}.svc.cluster.local`);
}
function transformWorkloads({ workloads, deploy, source, registryPrefix, runtimeEndpoint, webEndpoint, profile = "dev" }) {
const result = cloneJson(workloads);
const deployServices = new Map((deploy.services ?? []).map((service) => [service.serviceId, service]));
const namespace = namespaceNameForProfile(profile);
const profileLabel = runtimeLabelForProfile(profile);
const stableRuntimeLabels = {
"app.kubernetes.io/part-of": "hwlab",
"hwlab.pikastech.local/environment": "dev",
"hwlab.pikastech.local/profile": "dev"
"hwlab.pikastech.local/environment": profileLabel,
"hwlab.pikastech.local/profile": profileLabel
};
for (const item of asItems(result, "workloads")) {
item.metadata.namespace = deploy.namespace;
item.metadata.namespace = namespace;
label(item.metadata, {
...stableRuntimeLabels,
"hwlab.pikastech.local/gitops-target": "g14",
"hwlab.pikastech.local/source-commit": source.full,
"unidesk.ai/g14-staging": "true"
"unidesk.ai/g14-staging": profile === "dev" ? "true" : "false"
});
annotate(item.metadata, {
"hwlab.pikastech.local/gitops-note": "G14 GitOps target only; production traffic remains outside this target.",
"hwlab.pikastech.local/gitops-note": profile === "prod" ? "G14 PROD GitOps target." : "G14 DEV GitOps target.",
"hwlab.pikastech.local/source-commit": source.full
});
if (item.kind === "Job") {
@@ -234,15 +276,30 @@ function transformWorkloads({ workloads, deploy, source, registryPrefix, runtime
container.image = image;
container.imagePullPolicy = "IfNotPresent";
container.env ??= [];
for (const entry of container.env) {
if (Object.hasOwn(entry, "value")) entry.value = namespaceScopedHost(entry.value, namespace);
}
upsertEnv(container.env, "HWLAB_ENVIRONMENT", profileLabel);
upsertEnv(container.env, "HWLAB_COMMIT_ID", source.full);
upsertEnv(container.env, "HWLAB_IMAGE", image);
upsertEnv(container.env, "HWLAB_IMAGE_TAG", source.short);
upsertEnv(container.env, "HWLAB_GITOPS_TARGET", "g14");
upsertEnv(container.env, "HWLAB_GITOPS_PROFILE", profileLabel);
upsertEnv(container.env, "HWLAB_GITOPS_SOURCE_COMMIT", source.full);
if (serviceId === "hwlab-cloud-api" || serviceId === "hwlab-edge-proxy") {
upsertEnv(container.env, "HWLAB_PUBLIC_ENDPOINT", runtimeEndpoint);
}
if (serviceId === "hwlab-cloud-api") {
upsertEnv(container.env, "HWLAB_CODE_AGENT_DEFAULT_PROVIDER_PROFILE", "deepseek");
upsertEnv(container.env, "HWLAB_CODE_AGENT_DEEPSEEK_MODEL", deepSeekProfileModel);
upsertEnv(container.env, "HWLAB_CODE_AGENT_DEEPSEEK_BASE_URL", deepSeekBaseUrl(namespace));
upsertEnv(container.env, "HWLAB_CODE_AGENT_CODEX_API_MODEL", codexApiProfileModel);
upsertEnv(container.env, "HWLAB_CODE_AGENT_CODEX_API_BASE_URL", codexApiProfileBaseUrl);
upsertEnv(container.env, "NO_PROXY", `${namespace}.svc.cluster.local,.svc,.cluster.local,127.0.0.1,localhost,::1,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,172.26.26.227,172.26.0.0/16`);
upsertEnv(container.env, "no_proxy", `${namespace}.svc.cluster.local,.svc,.cluster.local,127.0.0.1,localhost,::1,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,172.26.26.227,172.26.0.0/16`);
}
if (serviceId === "hwlab-cloud-web") {
upsertEnv(container.env, "HWLAB_API_BASE_URL", `http://hwlab-cloud-api.${namespace}.svc.cluster.local:6667`);
upsertEnv(container.env, "HWLAB_PUBLIC_ENDPOINT", webEndpoint);
}
if (serviceId === "hwlab-cli") {
@@ -259,8 +316,14 @@ function transformWorkloads({ workloads, deploy, source, registryPrefix, runtime
return result;
}
function transformHealthContract(value, namespace, labels, annotations, runtimeEndpoint, webEndpoint) {
function transformHealthContract(value, namespace, labels, annotations, runtimeEndpoint, webEndpoint, profile = "dev") {
const result = transformListNamespace(value, namespace, labels, annotations);
if (result.metadata?.name === "hwlab-dev-health-contract") {
result.metadata.name = `hwlab-${profile}-health-contract`;
}
if (result.metadata?.labels && Object.hasOwn(result.metadata.labels, "app.kubernetes.io/name")) {
result.metadata.labels["app.kubernetes.io/name"] = `hwlab-${profile}-health-contract`;
}
if (result.data && typeof result.data === "object") {
if (Object.hasOwn(result.data, "endpoint")) result.data.endpoint = runtimeEndpoint;
if (Object.hasOwn(result.data, "cloud-web")) {
@@ -1194,26 +1257,31 @@ function argoProject() {
spec: {
description: "HWLAB G14 GitOps project; D601 remains outside this project.",
sourceRepos: [defaultSourceRepo],
destinations: [{ server: "https://kubernetes.default.svc", namespace: "hwlab-dev" }],
destinations: [
{ server: "https://kubernetes.default.svc", namespace: "hwlab-dev" },
{ server: "https://kubernetes.default.svc", namespace: "hwlab-prod" }
],
clusterResourceWhitelist: [{ group: "", kind: "Namespace" }],
namespaceResourceWhitelist: [{ group: "*", kind: "*" }]
}
};
}
function argoApplication(args) {
function argoApplication(args, profile = "dev") {
const namespace = namespaceNameForProfile(profile);
const runtimePath = runtimePathForProfile(profile);
return {
apiVersion: "argoproj.io/v1alpha1",
kind: "Application",
metadata: {
name: "hwlab-g14-dev",
name: `hwlab-g14-${profile}`,
namespace: "argocd",
labels: { "app.kubernetes.io/part-of": "hwlab", "hwlab.pikastech.local/gitops-target": "g14" }
},
spec: {
project: "hwlab-g14",
source: { repoURL: args.sourceRepo, targetRevision: args.gitopsBranch, path: "deploy/gitops/g14/runtime" },
destination: { server: "https://kubernetes.default.svc", namespace: "hwlab-dev" },
source: { repoURL: args.sourceRepo, targetRevision: args.gitopsBranch, path: `deploy/gitops/g14/${runtimePath}` },
destination: { server: "https://kubernetes.default.svc", namespace },
syncPolicy: { automated: { prune: false, selfHeal: true }, syncOptions: ["CreateNamespace=true", "ApplyOutOfSyncOnly=true"] }
}
};
@@ -1289,12 +1357,71 @@ remotePort = 17667
};
}
function runtimeKustomization({ source }) {
function deepSeekProxyManifest({ profile = "dev" } = {}) {
const namespace = namespaceNameForProfile(profile);
const labels = {
"app.kubernetes.io/name": "hwlab-deepseek-proxy",
"app.kubernetes.io/part-of": "hwlab",
"hwlab.pikastech.local/environment": runtimeLabelForProfile(profile),
"hwlab.pikastech.local/gitops-target": "g14",
"hwlab.pikastech.local/service-id": "hwlab-deepseek-proxy"
};
return {
apiVersion: "v1",
kind: "List",
items: [
{
apiVersion: "v1",
kind: "ConfigMap",
metadata: { name: "hwlab-deepseek-proxy-config", namespace, labels },
data: {
"config.yaml": `model_list:\n - model_name: ${deepSeekProfileModel}\n litellm_params:\n model: ${deepSeekLiteLlmModel}\n api_key: os.environ/DEEPSEEK_API_KEY\n api_base: https://api.deepseek.com\nlitellm_settings:\n drop_params: true\n set_verbose: false\n`
}
},
{
apiVersion: "apps/v1",
kind: "Deployment",
metadata: { name: "hwlab-deepseek-proxy", namespace, labels },
spec: {
replicas: 1,
selector: { matchLabels: { "app.kubernetes.io/name": "hwlab-deepseek-proxy" } },
template: {
metadata: { labels },
spec: {
containers: [{
name: "litellm",
image: deepSeekProxyImage,
imagePullPolicy: "IfNotPresent",
args: ["--config", "/etc/litellm/config.yaml", "--host", "0.0.0.0", "--port", "4000"],
env: [{ name: "DEEPSEEK_API_KEY", valueFrom: { secretKeyRef: { name: "hwlab-code-agent-provider", key: "openai-api-key", optional: true } } }],
ports: [{ name: "http", containerPort: 4000 }],
readinessProbe: { httpGet: { path: "/health/readiness", port: "http" }, initialDelaySeconds: 10, periodSeconds: 10 },
livenessProbe: { httpGet: { path: "/health/liveliness", port: "http" }, initialDelaySeconds: 20, periodSeconds: 20 },
volumeMounts: [{ name: "config", mountPath: "/etc/litellm", readOnly: true }]
}],
volumes: [{ name: "config", configMap: { name: "hwlab-deepseek-proxy-config" } }]
}
}
}
},
{
apiVersion: "v1",
kind: "Service",
metadata: { name: "hwlab-deepseek-proxy", namespace, labels },
spec: { type: "ClusterIP", selector: { "app.kubernetes.io/name": "hwlab-deepseek-proxy" }, ports: [{ name: "http", port: 4000, targetPort: "http" }] }
}
]
};
}
function runtimeKustomization({ profile = "dev" } = {}) {
const resources = ["namespace.yaml", "code-agent-codex-config.yaml", "services.yaml", "health-contract.yaml", "workloads.yaml", "deepseek-proxy.yaml"];
if (profile === "dev") resources.push("g14-frpc.yaml");
return {
apiVersion: "kustomize.config.k8s.io/v1beta1",
kind: "Kustomization",
namespace: "hwlab-dev",
resources: ["namespace.yaml", "code-agent-codex-config.yaml", "services.yaml", "health-contract.yaml", "workloads.yaml", "g14-frpc.yaml"]
namespace: namespaceNameForProfile(profile),
resources
};
}
@@ -1317,7 +1444,7 @@ This directory is generated from \`CI.json\`, \`deploy/deploy.json\`, and \`depl
}
async function plannedFiles(args) {
const [ci, deploy, namespace, config, services, health, workloads] = await Promise.all([
const [ci, deploy, namespaceTemplate, config, services, health, workloads] = await Promise.all([
readJson("CI.json"),
readJson("deploy/deploy.json"),
readJson("deploy/k8s/base/namespace.yaml"),
@@ -1327,12 +1454,8 @@ async function plannedFiles(args) {
readJson("deploy/k8s/base/workloads.yaml")
]);
const source = await resolveSourceRevision(args.sourceRevision);
const labels = { "hwlab.pikastech.local/gitops-target": "g14", "hwlab.pikastech.local/source-commit": source.full };
const baseLabels = { "hwlab.pikastech.local/gitops-target": "g14", "hwlab.pikastech.local/source-commit": source.full };
const annotations = { "hwlab.pikastech.local/rendered-by": "scripts/g14-gitops-render.mjs" };
const runtimeNamespace = cloneJson(namespace);
runtimeNamespace.metadata.name = deploy.namespace;
label(runtimeNamespace.metadata, labels);
annotate(runtimeNamespace.metadata, annotations);
const files = new Map();
const putJson = (relative, value) => files.set(path.join(args.outDir, relative), jsonManifest(value));
const putText = (relative, value) => files.set(path.join(args.outDir, relative), textFile(value));
@@ -1345,11 +1468,14 @@ async function plannedFiles(args) {
gitopsBranch: args.gitopsBranch,
sourceRepo: args.sourceRepo,
registryPrefix: args.registryPrefix,
runtimePath: "deploy/gitops/g14/runtime",
publicEndpoints: { web: args.webEndpoint, runtime: args.runtimeEndpoint },
runtimePaths: ["deploy/gitops/g14/runtime-dev", "deploy/gitops/g14/runtime-prod"],
publicEndpoints: {
dev: { web: args.webEndpoint, runtime: args.runtimeEndpoint },
prod: { web: args.prodWebEndpoint, runtime: args.prodRuntimeEndpoint }
},
frpDeployment: "hwlab-dev/hwlab-g14-frpc",
tektonPipeline: "hwlab-ci/hwlab-g14-ci-image-publish",
argoApplication: "argocd/hwlab-g14-dev"
argoApplications: ["argocd/hwlab-g14-dev", "argocd/hwlab-g14-prod"]
});
putText("README.md", readme({ source, args }));
putJson("registry/registry.yaml", registryManifest());
@@ -1359,14 +1485,27 @@ async function plannedFiles(args) {
putJson("tekton/control-plane-reconciler.yaml", tektonControlPlaneReconcilerCronJob(args));
putJson("tekton/pipelinerun.sample.yaml", tektonPipelineRunTemplate({ source, args }));
putJson("argocd/project.yaml", argoProject());
putJson("argocd/application.yaml", argoApplication(args));
putJson("runtime/kustomization.yaml", runtimeKustomization({ source }));
putJson("runtime/namespace.yaml", runtimeNamespace);
putJson("runtime/code-agent-codex-config.yaml", transformListNamespace(config, deploy.namespace, labels, annotations));
putJson("runtime/services.yaml", transformListNamespace(services, deploy.namespace, labels, annotations));
putJson("runtime/health-contract.yaml", transformHealthContract(health, deploy.namespace, labels, annotations, args.runtimeEndpoint, args.webEndpoint));
putJson("runtime/workloads.yaml", transformWorkloads({ workloads, deploy, source, registryPrefix: args.registryPrefix, runtimeEndpoint: args.runtimeEndpoint, webEndpoint: args.webEndpoint }));
putJson("runtime/g14-frpc.yaml", g14FrpcManifest());
putJson("argocd/application-dev.yaml", argoApplication(args, "dev"));
putJson("argocd/application-prod.yaml", argoApplication(args, "prod"));
for (const profile of ["dev", "prod"]) {
const namespace = namespaceNameForProfile(profile);
const profileLabels = { ...baseLabels, "hwlab.pikastech.local/environment": runtimeLabelForProfile(profile), "hwlab.pikastech.local/profile": runtimeLabelForProfile(profile) };
const runtimeNamespace = cloneJson(namespaceTemplate);
runtimeNamespace.metadata.name = namespace;
label(runtimeNamespace.metadata, profileLabels);
annotate(runtimeNamespace.metadata, annotations);
const runtimePath = runtimePathForProfile(profile);
const endpoints = profileEndpoint(args, profile);
putJson(`${runtimePath}/kustomization.yaml`, runtimeKustomization({ profile }));
putJson(`${runtimePath}/namespace.yaml`, runtimeNamespace);
putJson(`${runtimePath}/code-agent-codex-config.yaml`, transformListNamespace(config, namespace, profileLabels, annotations));
putJson(`${runtimePath}/services.yaml`, transformListNamespace(services, namespace, profileLabels, annotations));
putJson(`${runtimePath}/health-contract.yaml`, transformHealthContract(health, namespace, profileLabels, annotations, endpoints.runtimeEndpoint, endpoints.webEndpoint, profile));
putJson(`${runtimePath}/workloads.yaml`, transformWorkloads({ workloads, deploy, source, registryPrefix: args.registryPrefix, runtimeEndpoint: endpoints.runtimeEndpoint, webEndpoint: endpoints.webEndpoint, profile }));
putJson(`${runtimePath}/deepseek-proxy.yaml`, deepSeekProxyManifest({ profile }));
if (profile === "dev") putJson(`${runtimePath}/g14-frpc.yaml`, g14FrpcManifest());
}
return { files, source };
}
@@ -1264,6 +1264,57 @@ export async function runDevCloudWorkbenchLayoutSmoke(args = {}) {
safety: layoutSafety()
});
} catch (error) {
if (isBrowserExecutionEnvironmentError(error)) {
const summary = `Workbench layout smoke skipped because the browser could not run: ${error.message}`;
return sanitizeLayoutReport({
$schema: "https://hwlab.pikastech.local/schemas/dev-gate-report.schema.json",
$id: reportModeId("layout"),
reportVersion: "v1",
status: "skip",
issue: "pikasTech/HWLAB#273",
taskId: "dev-cloud-workbench-layout",
commitId: observeSourceIdentity().reportCommitId,
acceptanceLevel: "dev_cloud_workbench_layout",
devOnly: true,
prodDisabled: true,
reportLifecycle: layoutReportLifecycle(useLiveUrl, "blocked"),
task: "DC-DCSN-P0-2026-003",
mode: "layout-browser",
sourceMode: layoutSourceMode,
url,
generatedAt: new Date().toISOString(),
sourceContract: layoutSourceContract(),
validationCommands: layoutValidationCommands(),
localSmoke: layoutLocalSmoke("skip"),
dryRun: layoutDryRun(),
devPreconditions: layoutDevPreconditions(useLiveUrl, "blocked"),
evidenceLevel: useLiveUrl ? "BLOCKED" : "SOURCE",
devLive: false,
summary,
viewports: layoutViewports.map(({ id, width, height }) => ({ id, width, height })),
checks: [
{
id: "layout-browser-runtime",
status: "skip",
summary,
evidence: ["browser launch failed", "use G14 k3s/CI Playwright base image for browser smoke"]
}
],
blockers: [
{
type: "environment_blocker",
scope: "layout-browser-runtime",
status: "open",
summary
}
],
artifacts: {
screenshotDir: artifactRoot,
reportPath: args.reportPath ?? null
},
safety: layoutSafety()
});
}
return sanitizeLayoutReport({
$schema: "https://hwlab.pikastech.local/schemas/dev-gate-report.schema.json",
$id: reportModeId("layout"),
@@ -9206,6 +9257,11 @@ function escapeRegExp(value) {
return String(value).replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
}
function isBrowserExecutionEnvironmentError(error) {
const message = String(error?.message ?? error ?? "");
return /Executable doesn't exist|Please run the following command to download new browsers|does not support chromium|Host system is missing dependencies|browserType\.launch|Failed to launch|No usable sandbox|Missing X server|Target page, context or browser has been closed/iu.test(message);
}
export function printSmokeHelp() {
return {
status: "usage",