fix: route deepseek codex via moonbridge

This commit is contained in:
Codex
2026-05-26 10:27:25 +08:00
parent 0ba5b2c352
commit 986d8f5c07
6 changed files with 278 additions and 33 deletions
+104 -3
View File
@@ -36,6 +36,10 @@ const server = createServer(async (clientReq, clientRes) => {
await sendCodexModels(clientReq, clientRes, url);
return;
}
if (clientReq.method === "POST" && url.pathname === "/v1/responses") {
await proxyResponsesRequest(clientReq, clientRes, url);
return;
}
await proxyRequest(clientReq, clientRes, url);
} catch (error) {
sendJson(clientRes, 502, {
@@ -53,18 +57,55 @@ server.listen(port, "0.0.0.0", () => {
});
async function sendReadiness(res) {
const upstream = await fetchUpstreamJson("/health/readiness").catch((error) => ({ ok: false, status: 502, payload: { error: error.message } }));
const upstream = await fetchUpstreamReadiness();
sendJson(res, upstream.ok ? 200 : 503, {
...healthPayload(),
status: upstream.ok ? "ready" : "degraded",
upstream: {
ok: upstream.ok,
status: upstream.status,
source: upstream.source,
payload: upstream.payload
}
});
}
async function fetchUpstreamReadiness() {
const health = await fetchUpstreamJson("/health/readiness")
.then((result) => ({ ...result, source: "/health/readiness" }))
.catch((error) => ({ ok: false, status: 502, source: "/health/readiness", payload: { error: error.message } }));
if (health.ok) return health;
const models = await fetchUpstreamJson("/v1/models")
.then((result) => ({ ...result, source: "/v1/models" }))
.catch((error) => ({ ok: false, status: 502, source: "/v1/models", payload: { error: error.message } }));
const modelCount = codexModelsFromUpstream(models.payload).length;
if (models.ok && modelCount > 0) {
return {
...models,
payload: {
modelCount,
fallbackFrom: {
source: health.source,
status: health.status
}
}
};
}
return {
...health,
payload: {
...health.payload,
modelProbe: {
ok: models.ok,
status: models.status,
modelCount
}
}
};
}
async function sendCodexModels(clientReq, clientRes, url) {
const upstream = await fetchUpstreamJson(`${url.pathname}${url.search}`, clientReq.headers).catch((error) => ({ ok: false, status: 502, payload: { error: error.message } }));
const models = codexModelsFromUpstream(upstream.payload);
@@ -73,7 +114,7 @@ async function sendCodexModels(clientReq, clientRes, url) {
upstream: {
ok: upstream.ok,
status: upstream.status,
source: upstream.ok ? "litellm" : "fallback"
source: upstream.ok ? "upstream" : "fallback"
}
});
}
@@ -126,6 +167,65 @@ async function proxyRequest(clientReq, clientRes, url) {
upstreamReq.end(body);
}
async function proxyResponsesRequest(clientReq, clientRes, url) {
const startedAt = Date.now();
const originalBody = await readRequestBody(clientReq);
const { body, headers, decompressed, sanitizedToolTypes } = decodeRequestBody(originalBody, clientReq.headers, url.pathname);
await proxyPreparedBody({
clientReq,
clientRes,
url,
body,
headers,
startedAt,
logExtra: { decompressed, sanitizedToolTypes }
});
}
async function proxyPreparedBody({ clientReq, clientRes, url, body, headers, startedAt, logExtra = {} }) {
const targetUrl = new URL(`${url.pathname}${url.search}`, upstreamBaseUrl);
const requestImpl = targetUrl.protocol === "https:" ? httpsRequest : httpRequest;
const upstreamReq = requestImpl({
protocol: targetUrl.protocol,
hostname: targetUrl.hostname,
port: targetUrl.port || (targetUrl.protocol === "https:" ? 443 : 80),
method: clientReq.method,
path: `${targetUrl.pathname}${targetUrl.search}`,
headers
}, (upstreamRes) => {
const responseHeaders = filterResponseHeaders(upstreamRes.headers);
clientRes.writeHead(upstreamRes.statusCode || 502, responseHeaders);
upstreamRes.pipe(clientRes);
upstreamRes.on("end", () => {
process.stderr.write(JSON.stringify({
serviceId,
method: clientReq.method,
path: url.pathname,
status: upstreamRes.statusCode || null,
elapsedMs: Date.now() - startedAt,
...logExtra
}) + "\n");
});
});
upstreamReq.on("error", (error) => {
if (!clientRes.headersSent) {
sendJson(clientRes, 502, {
error: {
code: "deepseek_bridge_upstream_error",
message: "DeepSeek bridge upstream request failed",
reason: error.message
}
});
} else {
clientRes.end();
}
});
upstreamReq.end(body);
}
function decodeRequestBody(body, headers, pathname = "") {
const encoding = String(headers["content-encoding"] || "").trim().toLowerCase();
const decoded = encoding === "zstd" ? Buffer.from(decompress(new Uint8Array(body))) : body;
@@ -255,7 +355,8 @@ function healthPayload() {
status: "ok",
upstream: upstreamBaseUrl.origin,
model: defaultModel,
zstdRequestBody: "decode"
zstdRequestBody: "decode",
responsesTools: "drop-non-function"
};
}
@@ -30,11 +30,18 @@ test("DeepSeek Responses bridge decompresses Codex zstd body and maps models cat
body: zstdPayload
});
assert.equal(response.status, 200);
assert.equal(upstream.captured.url, "/v1/responses");
assert.equal(upstream.captured.encoding, null);
const forwarded = JSON.parse(upstream.captured.body);
assert.equal(forwarded.model, "deepseek-chat");
assert.deepEqual(forwarded.tools.map((tool) => tool.type), ["function", "function"]);
const readinessResponse = await fetch(`http://127.0.0.1:${bridge.port}/health/readiness`);
assert.equal(readinessResponse.status, 200);
const readiness = await readinessResponse.json();
assert.equal(readiness.upstream.source, "/v1/models");
assert.equal(readiness.upstream.payload.modelCount, 1);
const modelsResponse = await fetch(`http://127.0.0.1:${bridge.port}/v1/models?client_version=test`);
assert.equal(modelsResponse.status, 200);
const models = await modelsResponse.json();
@@ -49,6 +56,11 @@ test("DeepSeek Responses bridge decompresses Codex zstd body and maps models cat
async function startUpstream() {
let captured = null;
const server = createServer(async (request, response) => {
if ((request.url || "").startsWith("/health/readiness")) {
response.writeHead(404, { "content-type": "application/json" });
response.end(JSON.stringify({ error: "not implemented" }));
return;
}
if ((request.url || "").startsWith("/v1/models")) {
response.writeHead(200, { "content-type": "application/json" });
response.end(JSON.stringify({ data: [{ id: "deepseek-chat" }] }));
+23
View File
@@ -0,0 +1,23 @@
FROM golang:1.25-bookworm AS builder
ARG MOONBRIDGE_REPO=https://github.com/ZhiYi-R/moon-bridge.git
ARG MOONBRIDGE_REF=1b99888d3dae889b79ee602cb875c7907f7e76f2
ENV GOPROXY=https://goproxy.cn,direct
WORKDIR /src
RUN git clone --filter=blob:none "${MOONBRIDGE_REPO}" moon-bridge
WORKDIR /src/moon-bridge
RUN git checkout "${MOONBRIDGE_REF}" \
&& go mod download \
&& CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags="-s -w" -o /out/moonbridge ./cmd/moonbridge
FROM scratch
WORKDIR /app
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
COPY --from=builder /out/moonbridge /app/moonbridge
EXPOSE 4001
USER 65532:65532
ENTRYPOINT ["/app/moonbridge"]
+4 -2
View File
@@ -29,8 +29,10 @@ Codex token boundary 仍由授权路径把 `OPENAI_API_KEY` 注入到 DEV runtim
直接指向 public `api.openai.com`。G14 默认 `deepseek` profile 指向
`http://hwlab-deepseek-proxy.<namespace>.svc.cluster.local:4000/v1/responses`;该
Service 必须先进入 `hwlab-deepseek-responses-bridge`,由 bridge 解压 Codex
Responses 的 zstd request body、规范化 `/v1/models` 返回,再转发到同 Pod 内
LiteLLM 4001。`codex-api` profile 仍可指向
Responses 的 zstd request body、规范化 `/v1/models` 返回、丢弃非 `function` tool
再转发到同 Pod 内 Moon Bridge 4001。Moon Bridge 是 DeepSeek profile 的真实
Responses 转换和 prompt cache 保留层;不要在 HWLAB 中手写完整转换器替代它。
`codex-api` profile 仍可指向
`http://172.26.26.227:17680/v1/responses` 作为旧 Responses 通道。
Runner 不得尝试修补、读取、回显或替换该 Secret。若 DEV runtime 缺少该授权凭证注入,
+9 -8
View File
@@ -45,10 +45,11 @@ HWLAB 是 monorepoG14 CI/CD 加速必须按组件输入判断构建和滚动
- `scripts/g14-ci-plan.mjs` 是只读 planner,默认读取当前 `CI.json``deploy/deploy.json``deploy/artifact-catalog.dev.json`,输出 `affectedServices``reusedServices``componentCommitId``componentInputHash``dockerfileHash``baseImageDigest``buildArgsHash` 和原因;它不得修改 CI、deploy、catalog 或 GitOps 文件。
- 服务清单兼容顺序固定为:显式 `--services``deploy.services[]``deploy.k3s.serviceMappings[]``internal/protocol.SERVICE_IDS`。因此旧 `deploy/deploy.json` 形态和当前完整 `deploy.services[]` 形态都必须能被 planner 识别。
- `CI.json` 仍是 CI 命令与禁用项合同来源;可选新增 `g14CiPlan.components.<serviceId>` 覆盖 `componentPaths``sharedPaths``runtimeDeps``buildSystemPaths`,但不得要求删除或改写现有 `commands` / `forbidden` 字段。
- `scripts/g14-artifact-publish.mjs` 的 publish report 可以携带 planner 的 per-service 元数据;`scripts/refresh-artifact-catalog.mjs` 可以把这些字段复制进 `deploy/artifact-catalog.dev.json` 的对应 service。旧字段 `commitId``image``imageTag``digest``publishState` 语义不变,旧检查脚本不得因为新增字段失败
- `scripts/g14-gitops-render.mjs` 默认保持 legacy 兼容:不传新开关时,仍按 `--source-revision` 把所有 workload image 渲染为同一个 source commit tag,现有 `npm run g14:gitops:check` 必须继续通过
- 混合 desired state 只能通过显式 `--use-deploy-images``HWLAB_G14_USE_DEPLOY_IMAGES=1` 启用。启用后 workload 的 container image、`HWLAB_IMAGE``HWLAB_IMAGE_TAG` 和 pod template `source-commit` 来自 `deploy.services[]` 的 per-service image/env;全局 GitOps metadata 仍记录本次 source commit。未启用时禁止读取旧 catalog/digest 去改变 rollout 行为
- 后续 Tekton 并发化只能以 planner 输出作为输入,先做到每个 changed service 一个构建任务;unchanged service 复用 catalog digest,且不能改 pod template,避免无意义 rollout。没有完整 per-service desired state 证据时,必须回退当前全量 source commit render,而不是猜测复用
- `scripts/g14-artifact-publish.mjs` 默认启用组件级 lazy build:先运行 planner,再只构建/推送 `affectedServices``reusedServices` 必须从 `deploy/artifact-catalog.dev.json` 复用已有 sha256 digest;如果 catalog 没有可验证 digest,必须结构化阻塞并提示先跑全量 publish。`--full-build` 是兼容回退开关,用于强制构建/发布每个 required service
- `scripts/g14-artifact-publish.mjs` 的 publish report 必须携带 planner 的 per-service 元数据;`scripts/refresh-artifact-catalog.mjs` 把这些字段复制进 `deploy/artifact-catalog.dev.json``deploy/deploy.json` 的对应 service。旧字段 `commitId``image``imageTag``digest``publishState` 语义不变,旧检查脚本不得因为新增字段失败
- `scripts/g14-gitops-render.mjs` 默认启用混合 desired stateworkload 的 container image、`HWLAB_IMAGE``HWLAB_IMAGE_TAG` 和 pod template `source-commit` 来自 `deploy.services[]` 的 per-service image/env;全局 GitOps metadata 仍记录本次 source commit。`--legacy-source-images``HWLAB_G14_USE_DEPLOY_IMAGES=0` 是兼容回退开关,用于把所有 workload image 渲染为同一个 source commit tag
- G14 Tekton promotion 在推送 `G14-gitops` 前,必须用 publish report 刷新 workspace 内的 `deploy/deploy.json``deploy/artifact-catalog.dev.json``deploy/k8s/base/workloads.yaml`,并把该 per-service artifact truth 以 `chore: promote G14 GitOps source <short>` 提交回 `G14` source branchpoller 必须跳过这类生成提交,避免自触发循环。这样下一轮 planner 才能从最新 catalog 复用上一轮已构建服务,避免把未变服务回滚到旧 digest
- 后续 Tekton 并发化只能以 planner 输出作为输入,先做到每个 changed service 一个构建任务;unchanged service 复用 catalog digest,且不能改 pod template,避免无意义 rollout。没有完整 per-service desired state 证据时,必须使用 `--full-build``--legacy-source-images` 回退,而不是猜测复用。
## Code Agent Provider Profiles
@@ -57,14 +58,14 @@ G14 Code Agent 通过同一个 repo-owned Codex app-server stdio runner 承载
| Profile | 默认 | Model | Base URL | 说明 |
| --- | --- | --- | --- | --- |
| `deepseek` | 是 | `HWLAB_CODE_AGENT_DEEPSEEK_MODEL`,默认 `deepseek-chat` | `HWLAB_CODE_AGENT_DEEPSEEK_BASE_URL`,默认 `http://hwlab-deepseek-proxy.<namespace>.svc.cluster.local:4000/v1/responses` | G14 集群内 DeepSeek Responses bridgeService 4000 先进入 `hwlab-deepseek-responses-bridge`,再转发到同 Pod 内 LiteLLM 4001。 |
| `deepseek` | 是 | `HWLAB_CODE_AGENT_DEEPSEEK_MODEL`,默认 `deepseek-chat` | `HWLAB_CODE_AGENT_DEEPSEEK_BASE_URL`,默认 `http://hwlab-deepseek-proxy.<namespace>.svc.cluster.local:4000/v1/responses` | G14 集群内 DeepSeek Responses bridgeService 4000 先进入 `hwlab-deepseek-responses-bridge`,再转发到同 Pod 内 Moon Bridge 4001。 |
| `codex-api` | 否 | `HWLAB_CODE_AGENT_CODEX_API_MODEL`,默认 `gpt-5.5` | `HWLAB_CODE_AGENT_CODEX_API_BASE_URL`,默认 `http://172.26.26.227:17680/v1/responses` | 保留旧 Codex/OpenAI-compatible Responses API 通道。 |
`hwlab-cloud-api` 仍以 `HWLAB_CODE_AGENT_PROVIDER=codex-stdio` 运行。`HWLAB_CODE_AGENT_MODEL``HWLAB_CODE_AGENT_OPENAI_BASE_URL` 可作为 runtime-default 兜底,但前端默认 profile 是 `deepseek`,用户可以切到 `codex-api`。Codex app-server 启动参数必须同时设置 provider base URL、provider `name``model``review_model`,否则 Codex/LiteLLM 链路可能在模型目录或 `/v1/responses` 阶段退化成 `model=None`。旧 `codex-api` profile 指向 `172.26.26.227`cloud-api 的 `NO_PROXY/no_proxy` 必须包含该 host 或私网段,避免旧通道被 G14 外网代理误拦截。
`hwlab-cloud-api` 仍以 `HWLAB_CODE_AGENT_PROVIDER=codex-stdio` 运行。`HWLAB_CODE_AGENT_MODEL``HWLAB_CODE_AGENT_OPENAI_BASE_URL` 可作为 runtime-default 兜底,但前端默认 profile 是 `deepseek`,用户可以切到 `codex-api`。Codex app-server 启动参数必须同时设置 provider base URL、provider `name``model``review_model`,否则 Codex/DeepSeek 链路可能在模型目录或 `/v1/responses` 阶段退化成 `model=None`。旧 `codex-api` profile 指向 `172.26.26.227`cloud-api 的 `NO_PROXY/no_proxy` 必须包含该 host 或私网段,避免旧通道被 G14 外网代理误拦截。
Codex app-server 当前要求 provider `wire_api="responses"`,不得把 DeepSeek profile 切到旧 `chat` wire API。Codex 发往 `/v1/responses` 的请求体可能带 `Content-Encoding: zstd`,而 LiteLLM DeepSeek bridge 默认不能按 Codex 期望解压该请求体,也不能直接返回 Codex 期望的 `models` 目录格式。因此 GitOps 中的 `hwlab-deepseek-proxy` Pod 必须包含 repo-owned `hwlab-deepseek-responses-bridge` sidecarService 端口 4000 指向 bridgebridge zstd request body 解压、删除 `Content-Encoding`/重写 `Content-Length` 后转发给 4001 的 LiteLLM,并把 `GET /v1/models` 转成 Codex-style `{ "models": [...] }` 目录。DeepSeek API 只接受 `function` toolsbridge 必须在转发前丢弃 Codex Responses 请求里的非 `function` tool 类型(例如 `image_generation`),但不得移除 shell/apply-patch 等 function tools。模型、密钥和真实推理仍由 LiteLLM 管理;bridge 不新增业务 gate,也不得把兼容失败伪装成 SOURCE/legacy blocker。
Codex app-server 当前要求 provider `wire_api="responses"`,不得把 DeepSeek profile 切到旧 `chat` wire API。DeepSeek profile 的真实 Responses 转换层固定使用 Moon Bridge;不要在 HWLAB 里手写完整 Responses-to-Chat/Anthropic 转换器,因为这会破坏 Moon Bridge 对 prompt cache、tool-result 顺序和模型目录的成熟处理。Codex 发往 `/v1/responses` 的请求体可能带 `Content-Encoding: zstd`,而 Moon Bridge 不负责解压 Codex zstd body。因此 GitOps 中的 `hwlab-deepseek-proxy` Pod 必须包含 repo-owned `hwlab-deepseek-responses-bridge` sidecarService 端口 4000 指向 bridgebridge 只做 zstd request body 解压、删除 `Content-Encoding`/重写 `Content-Length`、丢弃非 `function` tool 类型和 Codex-style `GET /v1/models` 目录适配,再把请求转发给 4001 的 Moon Bridge。DeepSeek API 只接受 `function` toolsbridge 必须在转发前丢弃 Codex Responses 请求里的非 `function` tool 类型(例如 `web_search``image_generation`),但不得移除 shell/apply-patch 等 function tools。模型、cache、tool 调用顺序、密钥和真实推理仍由 Moon Bridge 管理;bridge 不新增业务 gate,也不得把兼容失败伪装成 SOURCE/legacy blocker。
DeepSeek proxy manifest 是 GitOps desired state 的一部分,DEV/PROD 分别生成在 `runtime-dev/deepseek-proxy.yaml``runtime-prod/deepseek-proxy.yaml`。DeepSeek key 仍通过 `hwlab-code-agent-provider/openai-api-key` Secret 注入到 proxy 容器;文档、trace、health、issue 和日志不得打印 Secret 值。
DeepSeek proxy manifest 是 GitOps desired state 的一部分,DEV/PROD 分别生成在 `runtime-dev/deepseek-proxy.yaml``runtime-prod/deepseek-proxy.yaml`Moon Bridge 镜像由 `deploy/moonbridge/Dockerfile` 从上游 `ZhiYi-R/moon-bridge` 固定 commit 构建,默认镜像为 G14 本地 registry 的 `moonbridge:<上游短 commit>`GitHub、Google 或 Docker base image 下载必须优先使用 G14 节点本地 proxy。DeepSeek key 仍通过 `hwlab-code-agent-provider/openai-api-key` Secret 注入到 init container 并写入 Pod 内 `emptyDir` 配置文件;ConfigMap、文档、trace、health、issue 和日志不得打印 Secret 值。
## Polling 触发
+126 -20
View File
@@ -24,8 +24,8 @@ const defaultNoProxy = process.env.HWLAB_G14_NO_PROXY || "127.0.0.1,localhost,::
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 moonBridgeSourceRef = process.env.HWLAB_G14_MOONBRIDGE_REF || "1b99888d3dae889b79ee602cb875c7907f7e76f2";
const moonBridgeImage = process.env.HWLAB_G14_MOONBRIDGE_IMAGE || `${defaultRegistryPrefix}/moonbridge:${moonBridgeSourceRef.slice(0, 12)}`;
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";
@@ -56,7 +56,7 @@ function parseArgs(argv) {
webEndpoint: defaultWebEndpoint,
prodRuntimeEndpoint: defaultProdRuntimeEndpoint,
prodWebEndpoint: defaultProdWebEndpoint,
useDeployImages: process.env.HWLAB_G14_USE_DEPLOY_IMAGES === "1",
useDeployImages: process.env.HWLAB_G14_USE_DEPLOY_IMAGES !== "0",
check: false,
write: true,
help: false
@@ -74,6 +74,7 @@ function parseArgs(argv) {
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 === "--use-deploy-images") args.useDeployImages = true;
else if (arg === "--legacy-source-images") args.useDeployImages = false;
else if (arg === "--check") {
args.check = true;
args.write = false;
@@ -107,7 +108,8 @@ function usage() {
` --web-endpoint URL default: ${defaultWebEndpoint}`,
` --prod-runtime-endpoint URL default: ${defaultProdRuntimeEndpoint}`,
` --prod-web-endpoint URL default: ${defaultProdWebEndpoint}`,
" --use-deploy-images render workload images/env from deploy/deploy.json per service; default keeps legacy source revision tags",
" --use-deploy-images default: render workload images/env from deploy/deploy.json per service",
" --legacy-source-images compatibility fallback: render every workload with the source revision tag",
" --check verify generated files are current without writing"
].join("\n");
}
@@ -596,23 +598,34 @@ test "$(git rev-parse HEAD)" = "$(params.revision)"
check_source_head() {
phase="$1"
expected="\${2:-$(params.revision)}"
latest="$(git ls-remote "$(params.git-url)" "refs/heads/$(params.source-branch)" | cut -f1)"
if [ -z "$latest" ]; then
echo '{"status":"failed","reason":"source-head-unresolved","phase":"'"$phase"'","sourceBranch":"'"$(params.source-branch)"'","sourceRevision":"'"$(params.revision)"'"}'
exit 1
fi
if [ "$latest" != "$(params.revision)" ]; then
echo '{"status":"skipped-stale-source","phase":"'"$phase"'","sourceBranch":"'"$(params.source-branch)"'","sourceRevision":"'"$(params.revision)"'","latestRevision":"'"$latest"'"}'
if [ "$latest" != "$expected" ]; then
echo '{"status":"skipped-stale-source","phase":"'"$phase"'","sourceBranch":"'"$(params.source-branch)"'","sourceRevision":"'"$(params.revision)"'","expectedRevision":"'"$expected"'","latestRevision":"'"$latest"'"}'
exit 0
fi
}
check_source_head before-render
node scripts/g14-gitops-render.mjs --source-revision "$(params.revision)" --source-repo "$(params.git-url)" --source-branch "$(params.source-branch)" --gitops-branch "$(params.gitops-branch)" --registry-prefix "$(params.registry-prefix)"
node scripts/g14-gitops-render.mjs --source-revision "$(params.revision)" --source-repo "$(params.git-url)" --source-branch "$(params.source-branch)" --gitops-branch "$(params.gitops-branch)" --registry-prefix "$(params.registry-prefix)" --check
check_source_head before-push
git config --global user.name "HWLAB G14 GitOps Bot"
git config --global user.email "hwlab-g14-gitops-bot@users.noreply.github.com"
if [ -s /workspace/source/dev-artifacts.json ]; then
node scripts/refresh-artifact-catalog.mjs --target-ref "$(params.revision)" --publish-report /workspace/source/dev-artifacts.json
if ! git diff --quiet -- deploy/deploy.json deploy/artifact-catalog.dev.json deploy/k8s/base/workloads.yaml; then
short="$(printf '%.7s' "$(params.revision)")"
git add deploy/deploy.json deploy/artifact-catalog.dev.json deploy/k8s/base/workloads.yaml
git commit -m "chore: promote G14 GitOps source $short"
git push origin "HEAD:$(params.source-branch)"
fi
fi
source_state_revision="$(git rev-parse HEAD)"
node scripts/g14-gitops-render.mjs --source-revision "$(params.revision)" --source-repo "$(params.git-url)" --source-branch "$(params.source-branch)" --gitops-branch "$(params.gitops-branch)" --registry-prefix "$(params.registry-prefix)" --use-deploy-images
node scripts/g14-gitops-render.mjs --source-revision "$(params.revision)" --source-repo "$(params.git-url)" --source-branch "$(params.source-branch)" --gitops-branch "$(params.gitops-branch)" --registry-prefix "$(params.registry-prefix)" --use-deploy-images --check
check_source_head before-push "$source_state_revision"
workdir="$(mktemp -d)"
git clone --no-checkout "$(params.git-url)" "$workdir/gitops"
cd "$workdir/gitops"
@@ -1410,19 +1423,30 @@ function deepSeekProxyManifest({ profile = "dev", source, registryPrefix = defau
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`
"render-config.sh": moonBridgeConfigRenderScript()
}
},
{
apiVersion: "apps/v1",
kind: "Deployment",
metadata: { name: "hwlab-deepseek-proxy", namespace, labels },
metadata: { name: "hwlab-deepseek-proxy", namespace, labels, annotations: { "hwlab.pikastech.local/moonbridge-source-ref": moonBridgeSourceRef } },
spec: {
replicas: 1,
selector: { matchLabels: { "app.kubernetes.io/name": "hwlab-deepseek-proxy" } },
template: {
metadata: { labels },
spec: {
initContainers: [{
name: "moonbridge-config",
image: bridgeImage,
imagePullPolicy: "IfNotPresent",
command: ["/bin/sh", "-eu", "/scripts/render-config.sh"],
env: [{ name: "DEEPSEEK_API_KEY", valueFrom: { secretKeyRef: { name: "hwlab-code-agent-provider", key: "openai-api-key", optional: true } } }],
volumeMounts: [
{ name: "moonbridge-scripts", mountPath: "/scripts", readOnly: true },
{ name: "moonbridge-config", mountPath: "/config" }
]
}],
containers: [{
name: "responses-bridge",
image: bridgeImage,
@@ -1437,17 +1461,23 @@ function deepSeekProxyManifest({ profile = "dev", source, registryPrefix = defau
readinessProbe: { httpGet: { path: "/health/readiness", port: "http" }, initialDelaySeconds: 10, periodSeconds: 10 },
livenessProbe: { httpGet: { path: "/health/liveliness", port: "http" }, initialDelaySeconds: 20, periodSeconds: 20 }
}, {
name: "litellm",
image: deepSeekProxyImage,
name: "moonbridge",
image: moonBridgeImage,
imagePullPolicy: "IfNotPresent",
args: ["--config", "/etc/litellm/config.yaml", "--host", "0.0.0.0", "--port", "4001"],
env: [{ name: "DEEPSEEK_API_KEY", valueFrom: { secretKeyRef: { name: "hwlab-code-agent-provider", key: "openai-api-key", optional: true } } }],
ports: [{ name: "litellm-http", containerPort: 4001 }],
readinessProbe: { httpGet: { path: "/health/readiness", port: "litellm-http" }, initialDelaySeconds: 10, periodSeconds: 10 },
livenessProbe: { httpGet: { path: "/health/liveliness", port: "litellm-http" }, initialDelaySeconds: 20, periodSeconds: 20 },
volumeMounts: [{ name: "config", mountPath: "/etc/litellm", readOnly: true }]
args: ["-config", "/config/config.yml"],
ports: [{ name: "moonbridge-http", containerPort: 4001 }],
readinessProbe: { httpGet: { path: "/v1/models", port: "moonbridge-http" }, initialDelaySeconds: 10, periodSeconds: 10 },
livenessProbe: { httpGet: { path: "/v1/models", port: "moonbridge-http" }, initialDelaySeconds: 20, periodSeconds: 20 },
volumeMounts: [
{ name: "moonbridge-config", mountPath: "/config", readOnly: true },
{ name: "moonbridge-data", mountPath: "/data" }
]
}],
volumes: [{ name: "config", configMap: { name: "hwlab-deepseek-proxy-config" } }]
volumes: [
{ name: "moonbridge-scripts", configMap: { name: "hwlab-deepseek-proxy-config" } },
{ name: "moonbridge-config", emptyDir: {} },
{ name: "moonbridge-data", emptyDir: {} }
]
}
}
}
@@ -1462,6 +1492,82 @@ function deepSeekProxyManifest({ profile = "dev", source, registryPrefix = defau
};
}
function moonBridgeConfigRenderScript() {
return `#!/bin/sh
if [ -z "\${DEEPSEEK_API_KEY:-}" ]; then
echo "DEEPSEEK_API_KEY is required" >&2
exit 1
fi
cat > /config/config.yml <<EOF
mode: "Transform"
log:
level: "info"
format: "text"
server:
addr: "0.0.0.0:4001"
persistence:
active_provider: db_sqlite
extensions:
db_sqlite:
enabled: true
config:
path: /data/moonbridge.db
wal: true
busy_timeout_ms: 5000
max_open_conns: 1
metrics:
enabled: true
config:
default_limit: 100
max_limit: 1000
deepseek_v4:
config:
reinforce_instructions: true
reinforce_prompt: "[System Reminder]: Please pay close attention to the system instructions, AGENTS.md files, and any other context provided. Follow them carefully and completely in your response.\\n[User]:"
cache:
mode: "explicit"
ttl: "5m"
prompt_caching: true
automatic_prompt_cache: false
explicit_cache_breakpoints: true
allow_retention_downgrade: false
max_breakpoints: 4
min_cache_tokens: 1024
expected_reuse: 2
minimum_value_score: 2048
min_breakpoint_tokens: 1024
defaults:
model: "${deepSeekProfileModel}"
max_tokens: 8192
trace:
enabled: false
models:
${deepSeekProfileModel}:
context_window: 128000
max_output_tokens: 8192
display_name: "${deepSeekProfileModel}"
description: "DeepSeek via Moon Bridge"
default_reasoning_level: null
supported_reasoning_levels: []
extensions:
deepseek_v4:
enabled: true
providers:
deepseek:
base_url: "https://api.deepseek.com/anthropic"
api_key: "\${DEEPSEEK_API_KEY}"
version: "2023-06-01"
user_agent: "moonbridge/1.0"
offers:
- model: ${deepSeekProfileModel}
routes:
${deepSeekProfileModel}:
model: ${deepSeekProfileModel}
provider: deepseek
EOF
`;
}
function deviceAgent71FreqServerScript() {
return [
"import http from \"node:http\";",