diff --git a/cmd/hwlab-deepseek-responses-bridge/main.mjs b/cmd/hwlab-deepseek-responses-bridge/main.mjs new file mode 100644 index 00000000..5d6db4f7 --- /dev/null +++ b/cmd/hwlab-deepseek-responses-bridge/main.mjs @@ -0,0 +1,243 @@ +#!/usr/bin/env node +import { createServer, request as httpRequest } from "node:http"; +import { request as httpsRequest } from "node:https"; + +import { decompress } from "fzstd"; + +const serviceId = "hwlab-deepseek-responses-bridge"; +const port = parsePositiveInteger(process.env.PORT, 4000); +const upstreamBaseUrl = new URL(process.env.HWLAB_DEEPSEEK_BRIDGE_UPSTREAM || "http://127.0.0.1:4001"); +const defaultModel = String(process.env.HWLAB_DEEPSEEK_BRIDGE_MODEL || process.env.HWLAB_CODE_AGENT_DEEPSEEK_MODEL || "deepseek-chat").trim() || "deepseek-chat"; +const maxBodyBytes = parsePositiveInteger(process.env.HWLAB_DEEPSEEK_BRIDGE_MAX_BODY_BYTES, 64 * 1024 * 1024); + +const hopByHopHeaders = new Set([ + "connection", + "keep-alive", + "proxy-authenticate", + "proxy-authorization", + "te", + "trailer", + "transfer-encoding", + "upgrade" +]); + +const server = createServer(async (clientReq, clientRes) => { + try { + const url = new URL(clientReq.url || "/", "http://hwlab-deepseek-bridge.local"); + if (url.pathname === "/health" || url.pathname === "/health/live" || url.pathname === "/health/liveliness") { + sendJson(clientRes, 200, healthPayload()); + return; + } + if (url.pathname === "/health/readiness") { + await sendReadiness(clientRes); + return; + } + if (clientReq.method === "GET" && (url.pathname === "/v1/models" || url.pathname === "/models")) { + await sendCodexModels(clientReq, clientRes, url); + return; + } + await proxyRequest(clientReq, clientRes, url); + } catch (error) { + sendJson(clientRes, 502, { + error: { + code: "deepseek_bridge_proxy_failed", + message: "DeepSeek Responses bridge failed to proxy request", + reason: error.message + } + }); + } +}); + +server.listen(port, "0.0.0.0", () => { + process.stdout.write(JSON.stringify({ serviceId, status: "listening", port, upstream: upstreamBaseUrl.origin }) + "\n"); +}); + +async function sendReadiness(res) { + const upstream = await fetchUpstreamJson("/health/readiness").catch((error) => ({ ok: false, status: 502, payload: { error: error.message } })); + sendJson(res, upstream.ok ? 200 : 503, { + ...healthPayload(), + status: upstream.ok ? "ready" : "degraded", + upstream: { + ok: upstream.ok, + status: upstream.status, + payload: upstream.payload + } + }); +} + +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); + sendJson(clientRes, 200, { + models: models.length > 0 ? models : [codexModel(defaultModel)], + upstream: { + ok: upstream.ok, + status: upstream.status, + source: upstream.ok ? "litellm" : "fallback" + } + }); +} + +async function proxyRequest(clientReq, clientRes, url) { + const startedAt = Date.now(); + const originalBody = await readRequestBody(clientReq); + const { body, headers, decompressed } = decodeRequestBody(originalBody, clientReq.headers); + 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, + decompressed, + elapsedMs: Date.now() - startedAt + }) + "\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) { + const encoding = String(headers["content-encoding"] || "").trim().toLowerCase(); + const decoded = encoding === "zstd" ? Buffer.from(decompress(new Uint8Array(body))) : body; + const forwarded = filterRequestHeaders(headers); + if (encoding === "zstd") delete forwarded["content-encoding"]; + forwarded["content-length"] = String(decoded.length); + forwarded.host = upstreamBaseUrl.host; + return { body: decoded, headers: forwarded, decompressed: encoding === "zstd" }; +} + +async function readRequestBody(req) { + const chunks = []; + let size = 0; + for await (const chunk of req) { + size += chunk.length; + if (size > maxBodyBytes) throw new Error(`request body exceeds ${maxBodyBytes} bytes`); + chunks.push(chunk); + } + return Buffer.concat(chunks); +} + +async function fetchUpstreamJson(pathAndSearch, headers = {}) { + const targetUrl = new URL(pathAndSearch, upstreamBaseUrl); + const response = await fetch(targetUrl, { + headers: { + accept: "application/json", + ...filterRequestHeaders(headers, { keepContentHeaders: false }), + host: upstreamBaseUrl.host + } + }); + const text = await response.text(); + let payload = null; + try { + payload = text ? JSON.parse(text) : null; + } catch { + payload = { text }; + } + return { ok: response.ok, status: response.status, payload }; +} + +function codexModelsFromUpstream(payload) { + if (payload && typeof payload === "object" && Array.isArray(payload.models)) { + return payload.models.map(normalizeCodexModel).filter(Boolean); + } + if (payload && typeof payload === "object" && Array.isArray(payload.data)) { + return payload.data + .map((item) => String(item?.id || item?.model_name || "").trim()) + .filter(Boolean) + .map(codexModel); + } + return []; +} + +function normalizeCodexModel(model) { + const slug = String(model?.slug || model?.id || "").trim(); + return slug ? { ...codexModel(slug), ...model, slug } : null; +} + +function codexModel(slug) { + return { + slug, + display_name: slug, + description: `${slug} via HWLAB DeepSeek bridge`, + default_reasoning_level: null, + supported_reasoning_levels: [], + shell_type: "shell_command", + visibility: "list", + supported_in_api: true, + priority: 0 + }; +} + +function filterRequestHeaders(headers, options = {}) { + const keepContentHeaders = options.keepContentHeaders !== false; + const result = {}; + for (const [name, value] of Object.entries(headers)) { + const lower = name.toLowerCase(); + if (hopByHopHeaders.has(lower)) continue; + if (lower === "host") continue; + if (!keepContentHeaders && (lower === "content-length" || lower === "content-type" || lower === "content-encoding")) continue; + result[lower] = value; + } + return result; +} + +function filterResponseHeaders(headers) { + const result = {}; + for (const [name, value] of Object.entries(headers)) { + if (!hopByHopHeaders.has(name.toLowerCase())) result[name] = value; + } + return result; +} + +function healthPayload() { + return { + serviceId, + status: "ok", + upstream: upstreamBaseUrl.origin, + model: defaultModel, + zstdRequestBody: "decode" + }; +} + +function sendJson(res, statusCode, payload) { + const body = JSON.stringify(payload, null, 2); + res.writeHead(statusCode, { + "content-type": "application/json; charset=utf-8", + "content-length": Buffer.byteLength(body) + }); + res.end(body); +} + +function parsePositiveInteger(value, fallback) { + const parsed = Number(value); + return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : fallback; +} diff --git a/cmd/hwlab-deepseek-responses-bridge/main.test.mjs b/cmd/hwlab-deepseek-responses-bridge/main.test.mjs new file mode 100644 index 00000000..230af67c --- /dev/null +++ b/cmd/hwlab-deepseek-responses-bridge/main.test.mjs @@ -0,0 +1,116 @@ +import assert from "node:assert/strict"; +import { spawn } from "node:child_process"; +import { createServer } from "node:http"; +import test from "node:test"; + +const zstdPayload = Buffer.from([ + 0x28, 0xb5, 0x2f, 0xfd, 0x04, 0x58, 0x49, 0x01, 0x00, 0x7b, 0x22, 0x6d, + 0x6f, 0x64, 0x65, 0x6c, 0x22, 0x3a, 0x22, 0x64, 0x65, 0x65, 0x70, 0x73, + 0x65, 0x65, 0x6b, 0x2d, 0x63, 0x68, 0x61, 0x74, 0x22, 0x2c, 0x22, 0x69, + 0x6e, 0x70, 0x75, 0x74, 0x22, 0x3a, 0x22, 0x68, 0x65, 0x6c, 0x6c, 0x6f, + 0x22, 0x7d, 0xc0, 0x09, 0xab, 0xba +]); +const decodedPayload = JSON.stringify({ model: "deepseek-chat", input: "hello" }); + +test("DeepSeek Responses bridge decompresses Codex zstd body and maps models catalog", async () => { + const upstream = await startUpstream(); + const bridge = await startBridge(upstream.port); + try { + const response = await fetch(`http://127.0.0.1:${bridge.port}/v1/responses`, { + method: "POST", + headers: { + "content-type": "application/json", + "content-encoding": "zstd" + }, + body: zstdPayload + }); + assert.equal(response.status, 200); + assert.equal(upstream.captured.encoding, null); + assert.equal(upstream.captured.body, decodedPayload); + + 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(); + assert.equal(Array.isArray(models.models), true); + assert.equal(models.models[0].slug, "deepseek-chat"); + } finally { + await bridge.stop(); + await upstream.stop(); + } +}); + +async function startUpstream() { + let captured = null; + const server = createServer(async (request, response) => { + if ((request.url || "").startsWith("/v1/models")) { + response.writeHead(200, { "content-type": "application/json" }); + response.end(JSON.stringify({ data: [{ id: "deepseek-chat" }] })); + return; + } + const chunks = []; + for await (const chunk of request) chunks.push(chunk); + captured = { + url: request.url, + method: request.method, + encoding: request.headers["content-encoding"] || null, + length: request.headers["content-length"] || null, + body: Buffer.concat(chunks).toString("utf8") + }; + response.writeHead(200, { "content-type": "application/json" }); + response.end(JSON.stringify({ ok: true })); + }); + await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); + return { + port: server.address().port, + get captured() { + return captured; + }, + stop: () => new Promise((resolve) => server.close(resolve)) + }; +} + +async function startBridge(upstreamPort) { + const port = await freePort(); + const child = spawn(process.execPath, ["cmd/hwlab-deepseek-responses-bridge/main.mjs"], { + cwd: process.cwd(), + env: { + ...process.env, + PORT: String(port), + HWLAB_DEEPSEEK_BRIDGE_UPSTREAM: `http://127.0.0.1:${upstreamPort}`, + HWLAB_DEEPSEEK_BRIDGE_MODEL: "deepseek-chat" + }, + stdio: ["ignore", "pipe", "pipe"] + }); + let stderr = ""; + child.stderr.on("data", (chunk) => { + stderr += chunk; + }); + await new Promise((resolve, reject) => { + const timer = setTimeout(() => reject(new Error(`bridge did not start: ${stderr}`)), 5000); + child.stdout.on("data", (chunk) => { + if (chunk.toString().includes("listening")) { + clearTimeout(timer); + resolve(); + } + }); + child.on("exit", (code) => { + clearTimeout(timer); + reject(new Error(`bridge exited early code=${code}: ${stderr}`)); + }); + }); + return { + port, + stop: async () => { + child.kill("SIGTERM"); + await new Promise((resolve) => child.once("exit", resolve)); + } + }; +} + +async function freePort() { + const server = createServer(); + await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); + const port = server.address().port; + await new Promise((resolve) => server.close(resolve)); + return port; +} diff --git a/docs/reference/code-agent-chat-readiness.md b/docs/reference/code-agent-chat-readiness.md index d930cdd7..85e32e7d 100644 --- a/docs/reference/code-agent-chat-readiness.md +++ b/docs/reference/code-agent-chat-readiness.md @@ -25,8 +25,13 @@ stdio session supervisor,并证明 `/workspace/hwlab` 可读写、`CODEX_HOME= 可创建和复用同一个 Codex thread/session。 Codex token boundary 仍由授权路径把 `OPENAI_API_KEY` 注入到 DEV runtime。DEV Pod -必须使用 `HWLAB_CODE_AGENT_OPENAI_BASE_URL` 指向受控 DEV egress/proxy 路径 -`http://172.26.26.227:17680/v1/responses`,不能直接指向 public `api.openai.com`。 +必须使用 `HWLAB_CODE_AGENT_OPENAI_BASE_URL` 指向受控 DEV egress/proxy 路径,不能 +直接指向 public `api.openai.com`。G14 默认 `deepseek` profile 指向 +`http://hwlab-deepseek-proxy..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 仍可指向 +`http://172.26.26.227:17680/v1/responses` 作为旧 Responses 通道。 Runner 不得尝试修补、读取、回显或替换该 Secret。若 DEV runtime 缺少该授权凭证注入, `provider_unavailable` 且 `error.missingEnv` 包含 `OPENAI_API_KEY` 必须判为 diff --git a/docs/reference/g14-gitops-cicd.md b/docs/reference/g14-gitops-cicd.md index b9face03..ad33b08e 100644 --- a/docs/reference/g14-gitops-cicd.md +++ b/docs/reference/g14-gitops-cicd.md @@ -45,10 +45,12 @@ 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..svc.cluster.local:4000/v1/responses` | G14 集群内 LiteLLM DeepSeek bridge。 | +| `deepseek` | 是 | `HWLAB_CODE_AGENT_DEEPSEEK_MODEL`,默认 `deepseek-chat` | `HWLAB_CODE_AGENT_DEEPSEEK_BASE_URL`,默认 `http://hwlab-deepseek-proxy..svc.cluster.local:4000/v1/responses` | G14 集群内 DeepSeek Responses bridge;Service 4000 先进入 `hwlab-deepseek-responses-bridge`,再转发到同 Pod 内 LiteLLM 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、`model` 和 `review_model`,否则 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/LiteLLM 链路可能在模型目录或 `/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` sidecar:Service 端口 4000 指向 bridge,bridge 对 zstd request body 解压、删除 `Content-Encoding`/重写 `Content-Length` 后转发给 4001 的 LiteLLM,并把 `GET /v1/models` 转成 Codex-style `{ "models": [...] }` 目录。模型、密钥和真实推理仍由 LiteLLM 管理;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 值。 diff --git a/internal/cloud/code-agent-session-registry.test.mjs b/internal/cloud/code-agent-session-registry.test.mjs index 7682b01c..bc07d00f 100644 --- a/internal/cloud/code-agent-session-registry.test.mjs +++ b/internal/cloud/code-agent-session-registry.test.mjs @@ -1224,6 +1224,7 @@ test("Codex app-server args pin repo-owned DEV responses egress without leaking assert.deepEqual(args.slice(-2), ["--listen", "stdio://"]); assert.ok(args.includes("model_provider=\"OpenAI\"")); assert.ok(args.includes("model_providers.OpenAI.base_url=\"http://172.26.26.227:17680/v1\"")); + assert.ok(args.includes("model_providers.OpenAI.name=\"OpenAI\"")); assert.ok(args.includes("model_providers.OpenAI.wire_api=\"responses\"")); assert.ok(args.includes("model=\"deepseek-chat\"")); assert.ok(args.includes("review_model=\"deepseek-chat\"")); diff --git a/internal/cloud/codex-stdio-session.mjs b/internal/cloud/codex-stdio-session.mjs index b2002938..404d5016 100644 --- a/internal/cloud/codex-stdio-session.mjs +++ b/internal/cloud/codex-stdio-session.mjs @@ -1210,6 +1210,8 @@ function codexAppServerProviderConfigArgs(env = process.env) { "-c", `model_providers.OpenAI.base_url=${tomlString(baseUrl)}`, "-c", + "model_providers.OpenAI.name=\"OpenAI\"", + "-c", "model_providers.OpenAI.wire_api=\"responses\"", "-c", "model_providers.OpenAI.requires_openai_auth=true" diff --git a/package-lock.json b/package-lock.json index 1f2ee411..52dc1e4b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.0.0-l0", "dependencies": { "@openai/codex": "^0.128.0", + "fzstd": "0.1.1", "pg": "^8.21.0", "playwright": "1.59.1" } @@ -149,6 +150,11 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/fzstd": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/fzstd/-/fzstd-0.1.1.tgz", + "integrity": "sha512-dkuVSOKKwh3eas5VkJy1AW1vFpet8TA/fGmVA5krThl8YcOVE/8ZIoEA1+U1vEn5ckxxhLirSdY837azmbaNHA==" + }, "node_modules/pg": { "version": "8.21.0", "resolved": "https://registry.npmjs.org/pg/-/pg-8.21.0.tgz", diff --git a/package.json b/package.json index 09517938..bd8a05c0 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "type": "module", "scripts": { "validate": "node scripts/repo-reports-guard.mjs && node scripts/validate-contract.mjs && node scripts/deploy-contract-plan.mjs --check && node scripts/deploy-desired-state-plan.mjs --check && node scripts/dev-runtime-provisioning.mjs --check && node scripts/dev-runtime-migration.mjs --check && node scripts/dev-runtime-postflight.mjs --check && node scripts/dev-runtime-hotfix-audit.mjs && node scripts/rpt004-mvp-e2e-harness.mjs --check --no-write && node --check scripts/artifact-runtime-readiness-guard.mjs && node --check scripts/src/artifact-runtime-readiness-guard.mjs && node --check scripts/dev-runtime-hotfix-audit.mjs && node --check scripts/src/dev-runtime-hotfix-audit.mjs && node --test scripts/artifact-runtime-readiness-guard.test.mjs scripts/src/dev-runtime-hotfix-audit.test.mjs", - "check": "node scripts/repo-reports-guard.mjs && node --check scripts/repo-reports-guard.mjs && node --check scripts/src/report-paths.mjs && node --check internal/protocol/index.mjs && node --check internal/build-metadata.mjs && node --check internal/agent/index.mjs && node --check internal/agent/runtime.mjs && node --check internal/audit/index.mjs && node --check internal/db/runtime-store.mjs && node --check internal/db/runtime-store.test.mjs && node --check internal/mvp-gate/summary.mjs && node --check internal/cloud/db-contract.mjs && node --check internal/dev-entrypoint/cloud-web-routes.mjs && node --check internal/dev-entrypoint/cloud-web-proxy.mjs && node --check internal/dev-entrypoint/cloud-web-proxy.test.mjs && node --check internal/dev-entrypoint/http.mjs && node --check internal/dev-entrypoint/http.test.mjs && node --check internal/cloud/code-agent-contract.mjs && node --check internal/cloud/code-agent-session-registry.mjs && node --check internal/cloud/code-agent-session-registry.test.mjs && node --check internal/cloud/code-agent-trace-store.mjs && node --check internal/cloud/codex-stdio-session.mjs && node --check internal/cloud/json-rpc.mjs && node --check internal/cloud/code-agent-chat.mjs && node --check internal/cloud/m3-io-control.mjs && node --check internal/cloud/server.mjs && node --check internal/sim/model.mjs && node --check internal/sim/model.test.mjs && node --check internal/sim/http.mjs && node --check internal/sim/l2-runtime.mjs && node --check internal/patchpanel/model.mjs && node --check internal/patchpanel/runtime.mjs && node --check cmd/hwlab-cloud-api/main.mjs && node --check cmd/hwlab-cloud-api/provision.mjs && node --check cmd/hwlab-cloud-api/migrate.mjs && node --check cmd/hwlab-edge-proxy/main.mjs && node --check cmd/hwlab-agent-mgr/main.mjs && node --check cmd/hwlab-agent-worker/main.mjs && node --check cmd/hwlab-box-simu/main.mjs && node --check cmd/hwlab-gateway/main.mjs && node --check cmd/hwlab-gateway-simu/main.mjs && node --check tools/hwlab-gateway-shell.mjs && node --check scripts/cloud-api-runtime-smoke.mjs && node --check scripts/code-agent-chat-smoke.mjs && node --check scripts/dev-edge-health-smoke.mjs && node --check scripts/src/dev-edge-health-smoke-lib.mjs && node --check scripts/validate-contract.mjs && node --check scripts/deploy-contract-plan.test.mjs && node --check scripts/deploy-desired-state-plan.mjs && node --check scripts/src/deploy-desired-state-plan.mjs && node --check scripts/artifact-runtime-readiness-guard.mjs && node --check scripts/src/artifact-runtime-readiness-guard.mjs && node --check scripts/artifact-runtime-readiness-guard.test.mjs && node --check scripts/report-lifecycle.mjs && node --check scripts/report-lifecycle.test.mjs && node --check scripts/validate-dev-gate-report.mjs && node --check scripts/dev-m3-hardware-loop-smoke.test.mjs && node --check scripts/m3-io-control-e2e.mjs && node --check scripts/src/m3-io-control-e2e.mjs && node --check scripts/m3-io-control-e2e.test.mjs && node --check scripts/dev-cloud-workbench-smoke.test.mjs && node --check scripts/dev-cloud-workbench-layout-smoke.mjs && node --check scripts/rpt004-mvp-e2e-harness.mjs && node --check scripts/src/rpt004-mvp-e2e-harness.mjs && node --check scripts/src/rpt004-mvp-e2e-harness.test.mjs && node --check scripts/validate-dev-m3-cardinality.mjs && node --check scripts/validate-dev-m3-cardinality.test.mjs && node --check scripts/validate-artifact-catalog.mjs && node --check scripts/refresh-artifact-catalog.mjs && node --check scripts/refresh-artifact-catalog.test.mjs && node --check scripts/artifact-publish.mjs && node --check scripts/g14-artifact-publish.mjs && node --check scripts/dev-runtime-base-image.mjs && node --check scripts/src/dev-artifact-services.mjs && node --check scripts/src/registry-capabilities.mjs && node --check scripts/preflight-dev-base-image.mjs && node --check scripts/src/dev-base-image-preflight.mjs && node --check scripts/dev-evidence-blocker-aggregator.mjs && node --check scripts/src/dev-evidence-blocker-aggregator.mjs && node --check scripts/src/dev-evidence-blocker-aggregator.test.mjs && node --check scripts/src/dev-m4-agent-loop-smoke-lib.test.mjs && node --check scripts/d601-k3s-readonly-observability.mjs && node --check scripts/src/d601-k3s-readonly-observability.mjs && node --check scripts/dev-runtime-provisioning.mjs && node --check scripts/src/dev-runtime-provisioning.mjs && node --check scripts/src/dev-runtime-provisioning.test.mjs && node --check scripts/dev-runtime-migration.mjs && node --check scripts/src/dev-runtime-migration.mjs && node --check scripts/src/dev-runtime-migration.test.mjs && node --check scripts/dev-runtime-postflight.mjs && node --check scripts/src/dev-runtime-postflight.mjs && node --check scripts/src/dev-runtime-postflight.test.mjs && node --check scripts/dev-runtime-hotfix-audit.mjs && node --check scripts/src/dev-runtime-hotfix-audit.mjs && node --check scripts/src/dev-runtime-hotfix-audit.test.mjs && node --test scripts/src/dev-runtime-hotfix-audit.test.mjs && node --check scripts/l2-runtime-contract-smoke.mjs && node --check scripts/patch-panel-runtime-smoke.mjs && node --check scripts/export-web-gate-summary.mjs && node --check scripts/l6-cli-web-smoke.mjs && node --check skills/hwlab-agent-runtime/scripts/hwlab-agent-runtime-cli.mjs && node --check skills/hwlab-agent-runtime/scripts/src/m3-io-skill-client.mjs && node --check tools/hwlab-cli/bin/hwlab-cli.mjs && node --check tools/hwlab-cli/lib/cli.mjs && node --check tools/hwlab-cli/lib/cli.test.mjs && node --check web/hwlab-cloud-web/app.mjs && node --check web/hwlab-cloud-web/code-agent-facts.mjs && node --check web/hwlab-cloud-web/code-agent-facts.test.mjs && node --check web/hwlab-cloud-web/code-agent-status.mjs && node --check web/hwlab-cloud-web/code-agent-status.test.mjs && node --check web/hwlab-cloud-web/code-agent-m3-evidence.mjs && node --check web/hwlab-cloud-web/live-status.mjs && node --check web/hwlab-cloud-web/wiring-status.mjs && node --check web/hwlab-cloud-web/gate-summary.mjs && node --check web/hwlab-cloud-web/scripts/check.mjs && node --check web/hwlab-cloud-web/scripts/live-status-contract.test.mjs && node --check web/hwlab-cloud-web/scripts/build.mjs && node --check web/hwlab-cloud-web/scripts/dist-contract.mjs && node scripts/validate-contract.mjs && node scripts/validate-dev-gate-report.mjs && node scripts/report-lifecycle.test.mjs && node scripts/validate-dev-m3-cardinality.mjs && node scripts/validate-artifact-catalog.mjs && node scripts/deploy-desired-state-plan.mjs --check && node scripts/dev-runtime-provisioning.mjs --check && node scripts/dev-runtime-migration.mjs --check && node cmd/hwlab-cloud-api/provision.mjs --check && node cmd/hwlab-cloud-api/migrate.mjs --check && node scripts/dev-runtime-postflight.mjs --check && node scripts/rpt004-mvp-e2e-harness.mjs --check --no-write && node scripts/dev-evidence-blocker-aggregator.mjs --check && node scripts/l2-runtime-contract-smoke.mjs && node scripts/l6-cli-web-smoke.mjs && node web/hwlab-cloud-web/scripts/check.mjs && node scripts/code-agent-chat-smoke.mjs && node scripts/cloud-api-runtime-smoke.mjs && sh -n scripts/bootstrap-skills.sh scripts/worker-entrypoint.sh", + "check": "node scripts/repo-reports-guard.mjs && node --check scripts/repo-reports-guard.mjs && node --check scripts/src/report-paths.mjs && node --check internal/protocol/index.mjs && node --check internal/build-metadata.mjs && node --check internal/agent/index.mjs && node --check internal/agent/runtime.mjs && node --check internal/audit/index.mjs && node --check internal/db/runtime-store.mjs && node --check internal/db/runtime-store.test.mjs && node --check internal/mvp-gate/summary.mjs && node --check internal/cloud/db-contract.mjs && node --check internal/dev-entrypoint/cloud-web-routes.mjs && node --check internal/dev-entrypoint/cloud-web-proxy.mjs && node --check internal/dev-entrypoint/cloud-web-proxy.test.mjs && node --check internal/dev-entrypoint/http.mjs && node --check internal/dev-entrypoint/http.test.mjs && node --check internal/cloud/code-agent-contract.mjs && node --check internal/cloud/code-agent-session-registry.mjs && node --check internal/cloud/code-agent-session-registry.test.mjs && node --check internal/cloud/code-agent-trace-store.mjs && node --check internal/cloud/codex-stdio-session.mjs && node --check internal/cloud/json-rpc.mjs && node --check internal/cloud/code-agent-chat.mjs && node --check internal/cloud/m3-io-control.mjs && node --check internal/cloud/server.mjs && node --check internal/sim/model.mjs && node --check internal/sim/model.test.mjs && node --check internal/sim/http.mjs && node --check internal/sim/l2-runtime.mjs && node --check internal/patchpanel/model.mjs && node --check internal/patchpanel/runtime.mjs && node --check cmd/hwlab-cloud-api/main.mjs && node --check cmd/hwlab-cloud-api/provision.mjs && node --check cmd/hwlab-cloud-api/migrate.mjs && node --check cmd/hwlab-deepseek-responses-bridge/main.mjs && node --check cmd/hwlab-deepseek-responses-bridge/main.test.mjs && node --test cmd/hwlab-deepseek-responses-bridge/main.test.mjs && node --check cmd/hwlab-edge-proxy/main.mjs && node --check cmd/hwlab-agent-mgr/main.mjs && node --check cmd/hwlab-agent-worker/main.mjs && node --check cmd/hwlab-box-simu/main.mjs && node --check cmd/hwlab-gateway/main.mjs && node --check cmd/hwlab-gateway-simu/main.mjs && node --check tools/hwlab-gateway-shell.mjs && node --check scripts/cloud-api-runtime-smoke.mjs && node --check scripts/code-agent-chat-smoke.mjs && node --check scripts/dev-edge-health-smoke.mjs && node --check scripts/src/dev-edge-health-smoke-lib.mjs && node --check scripts/validate-contract.mjs && node --check scripts/deploy-contract-plan.test.mjs && node --check scripts/deploy-desired-state-plan.mjs && node --check scripts/src/deploy-desired-state-plan.mjs && node --check scripts/artifact-runtime-readiness-guard.mjs && node --check scripts/src/artifact-runtime-readiness-guard.mjs && node --check scripts/artifact-runtime-readiness-guard.test.mjs && node --check scripts/report-lifecycle.mjs && node --check scripts/report-lifecycle.test.mjs && node --check scripts/validate-dev-gate-report.mjs && node --check scripts/dev-m3-hardware-loop-smoke.test.mjs && node --check scripts/m3-io-control-e2e.mjs && node --check scripts/src/m3-io-control-e2e.mjs && node --check scripts/m3-io-control-e2e.test.mjs && node --check scripts/dev-cloud-workbench-smoke.test.mjs && node --check scripts/dev-cloud-workbench-layout-smoke.mjs && node --check scripts/rpt004-mvp-e2e-harness.mjs && node --check scripts/src/rpt004-mvp-e2e-harness.mjs && node --check scripts/src/rpt004-mvp-e2e-harness.test.mjs && node --check scripts/validate-dev-m3-cardinality.mjs && node --check scripts/validate-dev-m3-cardinality.test.mjs && node --check scripts/validate-artifact-catalog.mjs && node --check scripts/refresh-artifact-catalog.mjs && node --check scripts/refresh-artifact-catalog.test.mjs && node --check scripts/artifact-publish.mjs && node --check scripts/g14-artifact-publish.mjs && node --check scripts/dev-runtime-base-image.mjs && node --check scripts/src/dev-artifact-services.mjs && node --check scripts/src/registry-capabilities.mjs && node --check scripts/preflight-dev-base-image.mjs && node --check scripts/src/dev-base-image-preflight.mjs && node --check scripts/dev-evidence-blocker-aggregator.mjs && node --check scripts/src/dev-evidence-blocker-aggregator.mjs && node --check scripts/src/dev-evidence-blocker-aggregator.test.mjs && node --check scripts/src/dev-m4-agent-loop-smoke-lib.test.mjs && node --check scripts/d601-k3s-readonly-observability.mjs && node --check scripts/src/d601-k3s-readonly-observability.mjs && node --check scripts/dev-runtime-provisioning.mjs && node --check scripts/src/dev-runtime-provisioning.mjs && node --check scripts/src/dev-runtime-provisioning.test.mjs && node --check scripts/dev-runtime-migration.mjs && node --check scripts/src/dev-runtime-migration.mjs && node --check scripts/src/dev-runtime-migration.test.mjs && node --check scripts/dev-runtime-postflight.mjs && node --check scripts/src/dev-runtime-postflight.mjs && node --check scripts/src/dev-runtime-postflight.test.mjs && node --check scripts/dev-runtime-hotfix-audit.mjs && node --check scripts/src/dev-runtime-hotfix-audit.mjs && node --check scripts/src/dev-runtime-hotfix-audit.test.mjs && node --test scripts/src/dev-runtime-hotfix-audit.test.mjs && node --check scripts/l2-runtime-contract-smoke.mjs && node --check scripts/patch-panel-runtime-smoke.mjs && node --check scripts/export-web-gate-summary.mjs && node --check scripts/l6-cli-web-smoke.mjs && node --check skills/hwlab-agent-runtime/scripts/hwlab-agent-runtime-cli.mjs && node --check skills/hwlab-agent-runtime/scripts/src/m3-io-skill-client.mjs && node --check tools/hwlab-cli/bin/hwlab-cli.mjs && node --check tools/hwlab-cli/lib/cli.mjs && node --check tools/hwlab-cli/lib/cli.test.mjs && node --check web/hwlab-cloud-web/app.mjs && node --check web/hwlab-cloud-web/code-agent-facts.mjs && node --check web/hwlab-cloud-web/code-agent-facts.test.mjs && node --check web/hwlab-cloud-web/code-agent-status.mjs && node --check web/hwlab-cloud-web/code-agent-status.test.mjs && node --check web/hwlab-cloud-web/code-agent-m3-evidence.mjs && node --check web/hwlab-cloud-web/live-status.mjs && node --check web/hwlab-cloud-web/wiring-status.mjs && node --check web/hwlab-cloud-web/gate-summary.mjs && node --check web/hwlab-cloud-web/scripts/check.mjs && node --check web/hwlab-cloud-web/scripts/live-status-contract.test.mjs && node --check web/hwlab-cloud-web/scripts/build.mjs && node --check web/hwlab-cloud-web/scripts/dist-contract.mjs && node scripts/validate-contract.mjs && node scripts/validate-dev-gate-report.mjs && node scripts/report-lifecycle.test.mjs && node scripts/validate-dev-m3-cardinality.mjs && node scripts/validate-artifact-catalog.mjs && node scripts/deploy-desired-state-plan.mjs --check && node scripts/dev-runtime-provisioning.mjs --check && node scripts/dev-runtime-migration.mjs --check && node cmd/hwlab-cloud-api/provision.mjs --check && node cmd/hwlab-cloud-api/migrate.mjs --check && node scripts/dev-runtime-postflight.mjs --check && node scripts/rpt004-mvp-e2e-harness.mjs --check --no-write && node scripts/dev-evidence-blocker-aggregator.mjs --check && node scripts/l2-runtime-contract-smoke.mjs && node scripts/l6-cli-web-smoke.mjs && node web/hwlab-cloud-web/scripts/check.mjs && node scripts/code-agent-chat-smoke.mjs && node scripts/cloud-api-runtime-smoke.mjs && sh -n scripts/bootstrap-skills.sh scripts/worker-entrypoint.sh", "dev-base-image:preflight": "node scripts/preflight-dev-base-image.mjs", "dev-runtime-base:build": "node scripts/dev-runtime-base-image.mjs", "cloud-api:smoke": "node scripts/cloud-api-runtime-smoke.mjs", @@ -51,6 +51,7 @@ }, "dependencies": { "@openai/codex": "^0.128.0", + "fzstd": "0.1.1", "pg": "^8.21.0", "playwright": "1.59.1" } diff --git a/scripts/artifact-publish.mjs b/scripts/artifact-publish.mjs index 1a724fdd..04b72411 100644 --- a/scripts/artifact-publish.mjs +++ b/scripts/artifact-publish.mjs @@ -845,7 +845,7 @@ function dockerfile(baseImage, port) { "ENV HWLAB_CODE_AGENT_SKILLS_DIRS=/app/skills:/root/.agents/skills:/home/ubuntu/.agents/skills", "COPY package.json ./package.json", "COPY package-lock.json* ./", - "RUN if [ -d /opt/hwlab-node-runtime-base/node_modules ]; then cp -a /opt/hwlab-node-runtime-base/node_modules ./node_modules; elif [ -f package-lock.json ]; then npm ci --omit=dev --include=optional --ignore-scripts; else npm install --omit=dev --include=optional --ignore-scripts; fi && codex_version=\"$(node -p \"require('./node_modules/@openai/codex/package.json').version\")\" && case \"$(uname -m)\" in x86_64|amd64) test -e node_modules/@openai/codex-linux-x64 || npm install --omit=dev --include=optional --ignore-scripts \"@openai/codex-linux-x64@npm:@openai/codex@${codex_version}-linux-x64\" ;; aarch64|arm64) test -e node_modules/@openai/codex-linux-arm64 || npm install --omit=dev --include=optional --ignore-scripts \"@openai/codex-linux-arm64@npm:@openai/codex@${codex_version}-linux-arm64\" ;; esac", + "RUN if [ -d /opt/hwlab-node-runtime-base/node_modules ]; then cp -a /opt/hwlab-node-runtime-base/node_modules ./node_modules; fi && if [ -f package-lock.json ]; then npm install --omit=dev --include=optional --ignore-scripts; else npm install --omit=dev --include=optional --ignore-scripts; fi && codex_version=\"$(node -p \"require('./node_modules/@openai/codex/package.json').version\")\" && case \"$(uname -m)\" in x86_64|amd64) test -e node_modules/@openai/codex-linux-x64 || npm install --omit=dev --include=optional --ignore-scripts \"@openai/codex-linux-x64@npm:@openai/codex@${codex_version}-linux-x64\" ;; aarch64|arm64) test -e node_modules/@openai/codex-linux-arm64 || npm install --omit=dev --include=optional --ignore-scripts \"@openai/codex-linux-arm64@npm:@openai/codex@${codex_version}-linux-arm64\" ;; esac", "COPY internal ./internal", "COPY cmd ./cmd", "COPY scripts ./scripts", diff --git a/scripts/code-agent-chat-smoke.mjs b/scripts/code-agent-chat-smoke.mjs index 28483d11..ecfde998 100644 --- a/scripts/code-agent-chat-smoke.mjs +++ b/scripts/code-agent-chat-smoke.mjs @@ -315,6 +315,7 @@ async function runLocalContractSmoke() { assert.equal(codexAppServerProviderBaseUrl(providerEnv), "http://172.26.26.227:17680/v1"); const providerArgs = codexAppServerArgs(providerEnv).join(" "); assert.match(providerArgs, /model_providers\.OpenAI\.base_url="http:\/\/172\.26\.26\.227:17680\/v1"/u); + assert.match(providerArgs, /model_providers\.OpenAI\.name="OpenAI"/u); assert.match(providerArgs, /model_providers\.OpenAI\.wire_api="responses"/u); assert.match(providerArgs, /model_providers\.OpenAI\.requires_openai_auth=true/u); assert.equal(providerArgs.includes("test-openai-key-material"), false); diff --git a/scripts/g14-gitops-render.mjs b/scripts/g14-gitops-render.mjs index 065f1599..9ad8561b 100644 --- a/scripts/g14-gitops-render.mjs +++ b/scripts/g14-gitops-render.mjs @@ -1362,8 +1362,9 @@ remotePort = ${edgeRemotePort} }; } -function deepSeekProxyManifest({ profile = "dev" } = {}) { +function deepSeekProxyManifest({ profile = "dev", source, registryPrefix = defaultRegistryPrefix } = {}) { const namespace = namespaceNameForProfile(profile); + const bridgeImage = imageFor(registryPrefix, "hwlab-cloud-api", source.short); const labels = { "app.kubernetes.io/name": "hwlab-deepseek-proxy", "app.kubernetes.io/part-of": "hwlab", @@ -1394,14 +1395,27 @@ function deepSeekProxyManifest({ profile = "dev" } = {}) { metadata: { labels }, spec: { containers: [{ + name: "responses-bridge", + image: bridgeImage, + imagePullPolicy: "IfNotPresent", + command: ["node", "/app/cmd/hwlab-deepseek-responses-bridge/main.mjs"], + env: [ + { name: "PORT", value: "4000" }, + { name: "HWLAB_DEEPSEEK_BRIDGE_UPSTREAM", value: "http://127.0.0.1:4001" }, + { name: "HWLAB_DEEPSEEK_BRIDGE_MODEL", value: deepSeekProfileModel } + ], + 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 } + }, { name: "litellm", image: deepSeekProxyImage, imagePullPolicy: "IfNotPresent", - args: ["--config", "/etc/litellm/config.yaml", "--host", "0.0.0.0", "--port", "4000"], + 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: "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 }, + 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 }] }], volumes: [{ name: "config", configMap: { name: "hwlab-deepseek-proxy-config" } }] @@ -1698,7 +1712,7 @@ async function plannedFiles(args) { 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 })); + putJson(`${runtimePath}/deepseek-proxy.yaml`, deepSeekProxyManifest({ profile, source, registryPrefix: args.registryPrefix })); if (includeDeviceAgent) putJson(`${runtimePath}/device-agent-71-freq.yaml`, deviceAgent71FreqManifest({ profile, source, registryPrefix: args.registryPrefix })); putJson(`${runtimePath}/g14-frpc.yaml`, g14FrpcManifest({ profile })); }