fix: bridge deepseek codex responses
This commit is contained in:
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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.<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 仍可指向
|
||||
`http://172.26.26.227:17680/v1/responses` 作为旧 Responses 通道。
|
||||
|
||||
Runner 不得尝试修补、读取、回显或替换该 Secret。若 DEV runtime 缺少该授权凭证注入,
|
||||
`provider_unavailable` 且 `error.missingEnv` 包含 `OPENAI_API_KEY` 必须判为
|
||||
|
||||
@@ -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.<namespace>.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.<namespace>.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 值。
|
||||
|
||||
|
||||
@@ -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\""));
|
||||
|
||||
@@ -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"
|
||||
|
||||
Generated
+6
@@ -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",
|
||||
|
||||
+2
-1
File diff suppressed because one or more lines are too long
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 }));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user