fix(admin): repair model discovery and simplify model setup
This commit is contained in:
+17
-1
@@ -2492,7 +2492,7 @@
|
||||
|
||||
## BUG-146 | staging 后台写请求把 Caddy 上游协议误当公开来源
|
||||
|
||||
- 状态:resolved(local candidate,未 push / deploy)
|
||||
- 状态:resolved
|
||||
- 首次发现:2026-08-07
|
||||
- 最近更新:2026-08-07
|
||||
- 影响面:所有经 `requireAdminMutation` 的后台写接口、`admin.staging.jyotisha.chat` 模型管理写操作;普通 staging 用户域名、其他后台功能的通用 `ReasonActionModal` 与 production 未改动。
|
||||
@@ -2520,3 +2520,19 @@
|
||||
- 相关记录:BUG-124、BUG-135、BUG-145、BUG-146
|
||||
- 复发自:无
|
||||
- 修复版本:待本次 staging SHA
|
||||
|
||||
## BUG-148 | Node 22/24 all-address lookup 使模型发现 DNS pinning 失效
|
||||
|
||||
- 状态:resolved
|
||||
- 首次发现:2026-08-08
|
||||
- 最近更新:2026-08-08
|
||||
- 影响面:共享 `requestAllowedModelProvider` 的固定 DNS HTTPS 请求;OpenAI-compatible 供应商模型发现无法到达已通过公网 SSRF 校验的上游。支付网关、SSRF 边界、重定向拒绝、超时与响应大小限制未放宽。
|
||||
- 用户现象:staging Web 容器内使用同一 DNS pinning/request 链路请求模型列表时失败,脱敏错误为 `ERR_INVALID_IP_ADDRESS`,已固定的地址族为 IPv4。
|
||||
- 触发条件:Node 22/24 的 `https.request` / `net.connect` 以 `lookup` 选项 `all=true` 调用自定义 DNS callback;旧实现无条件调用 `callback(null, address, family)`,而 all-address 契约要求第二参数为 `[{ address, family }]`。
|
||||
- 根因:共享 HTTPS helper 只实现了旧的单地址 lookup callback 形态,没有根据 Node 传入的 `options.all` 切换返回值;因此安全校验和 DNS 解析均成功后,Node 在建连前把字符串结果按地址数组读取并抛出 `ERR_INVALID_IP_ADDRESS`。discover route 的 provider 读取、密钥解密、`/models` URL、响应解析和错误翻译均不是本次失败根因。
|
||||
- 修复:抽出 `pinnedAddressLookup` 供共享 HTTPS 请求使用;`options.all=true` 时返回单元素已验证地址数组,其他模式继续返回 `address, family`。仍只使用已通过完整公网校验的固定地址,不重新解析、不跟随重定向,也不删除任何 SSRF/信任边界校验。
|
||||
- 验证:`npx tsx --test tests/model-provider-encrypted-credentials.test.ts` 的真实本地 TCP 回归在修复前稳定失败为 `ERR_INVALID_IP_ADDRESS`(4 passed / 1 failed),修复后对 `autoSelectFamily=true` 的 all-address 模式和 `autoSelectFamily=false` 的单地址模式均实际建连成功(5 passed / 0 failed)。
|
||||
- 防复发:DNS-pinned 请求测试必须通过 Node 网络栈实际调用自定义 lookup,并同时覆盖地址数组与单地址 callback 契约;不得用只匹配源码字符串的断言替代该回归。
|
||||
- 相关记录:BUG-146、BUG-147
|
||||
- 复发自:无
|
||||
- 修复版本:2026-08-08 staging 变更
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
export async function createDiscoveredModelId(providerId: string, providerModel: string) {
|
||||
const digest = await globalThis.crypto.subtle.digest(
|
||||
"SHA-256",
|
||||
new TextEncoder().encode(`${providerId}\0${providerModel}`),
|
||||
);
|
||||
const hex = Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("");
|
||||
return `m-${hex.slice(0, 62)}`;
|
||||
}
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
|
||||
import { adminRequestJson, type AdminIdentity } from "@/lib/admin/providers";
|
||||
import { createDiscoveredModelId } from "./model-id";
|
||||
import { formatAdminDate } from "./resource-table";
|
||||
|
||||
const { Text } = Typography;
|
||||
@@ -72,10 +73,7 @@ type ProviderForm = {
|
||||
apiKey?: string;
|
||||
enabled: boolean;
|
||||
};
|
||||
type ModelForm = Omit<ModelVersion, "id" | "configId" | "version" | "providerCode" | "status" | "createdAt" | "publishedAt" | "settings"> & {
|
||||
versionId?: string;
|
||||
settingsJson: string;
|
||||
};
|
||||
type ModelForm = Pick<ModelVersion, "providerId" | "providerModel" | "creditCost" | "enabled" | "isDefault">;
|
||||
|
||||
type ModelsPayload = { data: ModelVersion[]; total: number; providers: Provider[] };
|
||||
type DiscoveredModel = { id: string; label?: string };
|
||||
@@ -106,7 +104,6 @@ export default function ModelManagement() {
|
||||
const [modelForm] = Form.useForm<ModelForm>();
|
||||
const [filterForm] = Form.useForm<ModelFilters>();
|
||||
const selectedProviderId = Form.useWatch("providerId", modelForm);
|
||||
const selectedProviderModel = Form.useWatch("providerModel", modelForm);
|
||||
const [models, setModels] = useState<ModelVersion[]>([]);
|
||||
const [providers, setProviders] = useState<Provider[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
@@ -167,38 +164,19 @@ export default function ModelManagement() {
|
||||
|
||||
function openModel(model?: ModelVersion, providerId?: string) {
|
||||
setEditingModel(model ?? null);
|
||||
setDiscoveredModels([]);
|
||||
setDiscoveredModels(model ? [{ id: model.providerModel, label: model.label }] : []);
|
||||
modelForm.setFieldsValue(model ? {
|
||||
modelId: model.modelId,
|
||||
versionId: model.id,
|
||||
providerId: model.providerId,
|
||||
label: model.label,
|
||||
description: model.description,
|
||||
providerModel: model.providerModel,
|
||||
modelTier: model.modelTier,
|
||||
creditCost: model.creditCost,
|
||||
contextWindow: model.contextWindow,
|
||||
inputCostMicrousdPerMillion: model.inputCostMicrousdPerMillion,
|
||||
outputCostMicrousdPerMillion: model.outputCostMicrousdPerMillion,
|
||||
enabled: model.enabled,
|
||||
isDefault: model.isDefault,
|
||||
fallbackModelId: model.fallbackModelId,
|
||||
settingsJson: JSON.stringify(model.settings, null, 2),
|
||||
} : {
|
||||
modelId: "",
|
||||
providerId: providerId ?? providers[0]?.id,
|
||||
label: "",
|
||||
description: "",
|
||||
providerModel: "",
|
||||
modelTier: "standard",
|
||||
creditCost: 1,
|
||||
contextWindow: null,
|
||||
inputCostMicrousdPerMillion: 0,
|
||||
outputCostMicrousdPerMillion: 0,
|
||||
enabled: false,
|
||||
isDefault: false,
|
||||
fallbackModelId: null,
|
||||
settingsJson: "{}",
|
||||
});
|
||||
setModelOpen(true);
|
||||
}
|
||||
@@ -239,7 +217,7 @@ export default function ModelManagement() {
|
||||
body: JSON.stringify({ providerId }),
|
||||
});
|
||||
setDiscoveredModels(payload.data);
|
||||
if (!payload.data.length) message.info("未发现可用模型,可继续手工输入");
|
||||
if (!payload.data.length) message.info("未发现可用模型,请检查供应商配置后重试");
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : "获取模型列表失败");
|
||||
} finally {
|
||||
@@ -247,46 +225,30 @@ export default function ModelManagement() {
|
||||
}
|
||||
}
|
||||
|
||||
function selectDiscoveredModel(providerModel: string) {
|
||||
const discovered = discoveredModels.find((item) => item.id === providerModel);
|
||||
const values = modelForm.getFieldsValue(["modelId", "label"]);
|
||||
modelForm.setFieldsValue({
|
||||
providerModel,
|
||||
...(!values.modelId ? {
|
||||
modelId: providerModel.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 64) || "model",
|
||||
} : {}),
|
||||
...(!values.label ? { label: discovered?.label?.trim() || providerModel } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
async function saveModel(values: ModelForm) {
|
||||
setSaving(true);
|
||||
try {
|
||||
let settings: unknown;
|
||||
try {
|
||||
settings = JSON.parse(values.settingsJson);
|
||||
} catch {
|
||||
throw new Error("设置 JSON 格式不正确");
|
||||
}
|
||||
const discovered = discoveredModels.find((item) => item.id === values.providerModel);
|
||||
const modelId = editingModel?.modelId ?? await createDiscoveredModelId(values.providerId, values.providerModel);
|
||||
await adminRequestJson("/api/admin/models", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
action: "saveDraft",
|
||||
modelId: values.modelId.trim(),
|
||||
modelId,
|
||||
versionId: editingModel?.id ?? null,
|
||||
providerId: values.providerId,
|
||||
label: values.label.trim(),
|
||||
description: values.description?.trim() ?? "",
|
||||
label: editingModel?.label ?? (discovered?.label?.trim() || values.providerModel).slice(0, 60),
|
||||
providerModel: values.providerModel.trim(),
|
||||
modelTier: values.modelTier,
|
||||
creditCost: values.creditCost,
|
||||
contextWindow: values.contextWindow ?? null,
|
||||
inputCostMicrousdPerMillion: values.inputCostMicrousdPerMillion,
|
||||
outputCostMicrousdPerMillion: values.outputCostMicrousdPerMillion,
|
||||
enabled: values.enabled,
|
||||
description: editingModel?.description ?? "",
|
||||
modelTier: editingModel?.modelTier ?? "standard",
|
||||
contextWindow: editingModel?.contextWindow ?? null,
|
||||
inputCostMicrousdPerMillion: editingModel?.inputCostMicrousdPerMillion ?? 0,
|
||||
outputCostMicrousdPerMillion: editingModel?.outputCostMicrousdPerMillion ?? 0,
|
||||
isDefault: values.isDefault,
|
||||
fallbackModelId: values.fallbackModelId?.trim() || null,
|
||||
settings,
|
||||
fallbackModelId: editingModel?.fallbackModelId ?? null,
|
||||
settings: editingModel?.settings ?? {},
|
||||
}),
|
||||
});
|
||||
message.success("模型草稿已保存");
|
||||
@@ -415,39 +377,40 @@ export default function ModelManagement() {
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
<Modal title={editingModel ? `编辑 ${editingModel.modelId}` : "新增模型草稿"} open={modelOpen} width={860} okText="保存草稿" cancelText="取消" confirmLoading={saving} onOk={() => modelForm.submit()} onCancel={() => setModelOpen(false)} destroyOnHidden>
|
||||
<Modal title={editingModel ? `编辑 ${editingModel.modelId}` : "新增模型草稿"} open={modelOpen} width={720} okText="保存草稿" cancelText="取消" confirmLoading={saving} onOk={() => modelForm.submit()} onCancel={() => setModelOpen(false)} destroyOnHidden>
|
||||
<Form<ModelForm> form={modelForm} layout="vertical" onFinish={saveModel}>
|
||||
<Row gutter={16}>
|
||||
<Col xs={24} md={8}><Form.Item name="modelId" label="模型 ID" rules={[{ required: true }, { pattern: /^[a-z0-9][a-z0-9._-]{0,63}$/ }]}><Input disabled={Boolean(editingModel)} /></Form.Item></Col>
|
||||
<Col xs={24} md={8}><Form.Item name="label" label="显示名称" rules={[{ required: true }]}><Input /></Form.Item></Col>
|
||||
<Col xs={24} md={8}><Form.Item label="供应商" required><Space.Compact block><Form.Item name="providerId" noStyle rules={[{ required: true, message: "请选择供应商" }]}><Select onChange={() => setDiscoveredModels([])} options={providers.map((item) => ({ value: item.id, label: `${item.name} (${item.code})` }))} /></Form.Item><Button loading={discovering} disabled={!selectedProviderId} onClick={() => void discoverModels(selectedProviderId)}>获取模型列表</Button></Space.Compact></Form.Item></Col>
|
||||
</Row>
|
||||
<Form.Item label="发现的模型" extra="选择后会填入供应商模型名;模型 ID 和显示名称仅在为空时自动补全。">
|
||||
<Form.Item label="供应商" required>
|
||||
<Space.Compact block>
|
||||
<Form.Item name="providerId" noStyle rules={[{ required: true, message: "请选择供应商" }]}>
|
||||
<Select
|
||||
onChange={() => {
|
||||
setDiscoveredModels([]);
|
||||
modelForm.setFieldValue("providerModel", "");
|
||||
}}
|
||||
options={providers.map((item) => ({ value: item.id, label: `${item.name} (${item.code})` }))}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Button loading={discovering} disabled={!selectedProviderId} onClick={() => void discoverModels(selectedProviderId)}>获取模型列表</Button>
|
||||
</Space.Compact>
|
||||
</Form.Item>
|
||||
<Form.Item name="providerModel" label="模型" rules={[{ required: true, message: "请从供应商模型列表中选择" }]} extra="获取列表后选择供应商提供的模型。">
|
||||
<Select
|
||||
showSearch
|
||||
allowClear
|
||||
optionFilterProp="label"
|
||||
placeholder={selectedProviderId ? "先获取模型列表,或在下方手工输入" : "请先选择供应商"}
|
||||
placeholder={selectedProviderId ? "获取并选择模型" : "请先选择供应商"}
|
||||
disabled={!selectedProviderId || !discoveredModels.length}
|
||||
value={discoveredModels.some((item) => item.id === selectedProviderModel) ? selectedProviderModel : undefined}
|
||||
options={discoveredModels.map((item) => ({ value: item.id, label: item.label ? `${item.label} (${item.id})` : item.id }))}
|
||||
onSelect={selectDiscoveredModel}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Row gutter={16}>
|
||||
<Col xs={24} md={12}><Form.Item name="providerModel" label="供应商模型名(可手工输入)" rules={[{ required: true }]}><Input /></Form.Item></Col>
|
||||
<Col xs={24} md={6}><Form.Item name="modelTier" label="模型档位" rules={[{ required: true }]}><Select options={Object.entries(modelTierLabels).map(([value, label]) => ({ value, label }))} /></Form.Item></Col>
|
||||
<Col xs={24} md={6}><Form.Item name="creditCost" label="单次点数" rules={[{ required: true }]}><InputNumber min={1} precision={0} style={{ width: "100%" }} /></Form.Item></Col>
|
||||
<Col xs={24} md={12}><Form.Item name="creditCost" label="单次点数" rules={[{ required: true }]}><InputNumber min={1} precision={0} style={{ width: "100%" }} /></Form.Item></Col>
|
||||
<Col xs={24} md={12}>
|
||||
<Space size="large">
|
||||
<Form.Item name="enabled" label="启用" valuePropName="checked"><Switch /></Form.Item>
|
||||
<Form.Item name="isDefault" label="默认模型" valuePropName="checked" dependencies={["enabled"]} rules={[({ getFieldValue }) => ({ validator(_, value) { return value && !getFieldValue("enabled") ? Promise.reject(new Error("默认模型必须启用")) : Promise.resolve(); } })]}><Switch /></Form.Item>
|
||||
</Space>
|
||||
</Col>
|
||||
</Row>
|
||||
<Form.Item name="description" label="说明"><Input.TextArea rows={2} /></Form.Item>
|
||||
<Row gutter={16}>
|
||||
<Col xs={24} md={8}><Form.Item name="contextWindow" label="上下文窗口"><InputNumber min={1} precision={0} style={{ width: "100%" }} /></Form.Item></Col>
|
||||
<Col xs={24} md={8}><Form.Item name="inputCostMicrousdPerMillion" label="输入成本(微美元/百万 Token)" rules={[{ required: true }]}><InputNumber min={0} precision={0} style={{ width: "100%" }} /></Form.Item></Col>
|
||||
<Col xs={24} md={8}><Form.Item name="outputCostMicrousdPerMillion" label="输出成本(微美元/百万 Token)" rules={[{ required: true }]}><InputNumber min={0} precision={0} style={{ width: "100%" }} /></Form.Item></Col>
|
||||
</Row>
|
||||
<Form.Item name="fallbackModelId" label="回退模型 ID"><Input allowClear /></Form.Item>
|
||||
<Form.Item name="settingsJson" label="设置 JSON" rules={[{ required: true }]}><Input.TextArea rows={5} spellCheck={false} /></Form.Item>
|
||||
<Space size="large"><Form.Item name="enabled" label="启用" valuePropName="checked"><Switch /></Form.Item><Form.Item name="isDefault" label="默认模型" valuePropName="checked" dependencies={["enabled"]} rules={[({ getFieldValue }) => ({ validator(_, value) { return value && !getFieldValue("enabled") ? Promise.reject(new Error("默认模型必须启用")) : Promise.resolve(); } })]}><Switch /></Form.Item></Space>
|
||||
</Form>
|
||||
</Modal>
|
||||
<Modal
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { promises as dns } from "node:dns";
|
||||
import https from "node:https";
|
||||
import { isIP } from "node:net";
|
||||
import { isIP, type LookupFunction } from "node:net";
|
||||
|
||||
const blockedHostnames = new Set([
|
||||
"localhost",
|
||||
@@ -17,6 +17,13 @@ type HostLookup = (
|
||||
) => Promise<readonly ResolvedAddress[]>;
|
||||
type ModelProviderEnvironment = Readonly<Record<string, string | undefined>>;
|
||||
|
||||
export function pinnedAddressLookup(pinned: ResolvedAddress): LookupFunction {
|
||||
return (_hostname, options, callback) => {
|
||||
if (options.all) callback(null, [pinned]);
|
||||
else callback(null, pinned.address, pinned.family);
|
||||
};
|
||||
}
|
||||
|
||||
function blockedIpv4(address: string) {
|
||||
const parts = address.split(".").map(Number);
|
||||
if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) return true;
|
||||
@@ -166,7 +173,7 @@ async function requestPinnedHttps(
|
||||
agent: false,
|
||||
headers,
|
||||
servername: isIP(hostname) ? undefined : hostname,
|
||||
lookup: (_hostname, _options, callback) => callback(null, pinned.address, pinned.family),
|
||||
lookup: pinnedAddressLookup(pinned),
|
||||
}, (response) => {
|
||||
const status = response.statusCode ?? 0;
|
||||
if (status >= 300 && status < 400) {
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { createDiscoveredModelId } from "../src/components/admin/model-id.ts";
|
||||
|
||||
const providerA = "11111111-1111-4111-8111-111111111111";
|
||||
const providerB = "22222222-2222-4222-8222-222222222222";
|
||||
|
||||
test("discovered model IDs are deterministic, distinct, and API-compatible", async () => {
|
||||
const inputs = [
|
||||
[providerA, "gpt-4o"],
|
||||
[providerB, "gpt-4o"],
|
||||
[providerA, "GPT-4o"],
|
||||
[providerA, " gpt-4o"],
|
||||
[providerA, "gpt-4o "],
|
||||
[providerA, "模型-α"],
|
||||
] as const;
|
||||
const ids = await Promise.all(inputs.map(([providerId, providerModel]) => createDiscoveredModelId(providerId, providerModel)));
|
||||
|
||||
assert.equal(new Set(ids).size, inputs.length);
|
||||
assert.equal(ids[0], "m-f1966f65ba46e2c192e274f77c120dbc90f32e6caa2bcd75bb52e4f985b8cb");
|
||||
assert.equal(await createDiscoveredModelId(providerA, "gpt-4o"), ids[0]);
|
||||
for (const id of ids) {
|
||||
assert.equal(id.length, 64);
|
||||
assert.match(id, /^m-[0-9a-f]{62}$/);
|
||||
}
|
||||
});
|
||||
@@ -23,17 +23,47 @@ test("provider form keeps codes server-owned and API keys write-only", () => {
|
||||
assert.match(component, /anthropic: "Anthropic 官方"/);
|
||||
});
|
||||
|
||||
test("model discovery stays searchable with a manual provider model fallback", () => {
|
||||
test("model discovery is the only model picker and generates system fields", () => {
|
||||
assert.match(component, /onClick=\{\(\) => void discoverModels\(selectedProviderId\)\}>获取模型列表<\/Button>/);
|
||||
assert.doesNotMatch(component, /title="获取供应商模型列表"[\s\S]*ReasonActionModal/);
|
||||
assert.match(component, /adminRequestJson<DiscoveredModelsPayload>\("\/api\/admin\/models\/discover"/);
|
||||
assert.match(component, /body: JSON\.stringify\(\{ providerId \}\)/);
|
||||
assert.match(component, /获取模型列表/);
|
||||
assert.match(component, /<Select[\s\S]*showSearch[\s\S]*optionFilterProp="label"[\s\S]*onSelect=\{selectDiscoveredModel\}/);
|
||||
assert.match(component, /name="providerModel" label="供应商模型名(可手工输入)"/);
|
||||
assert.match(component, /providerModel\.toLowerCase\(\)\.replace\(\/\[\^a-z0-9\]\+\/g, "-"\)/);
|
||||
assert.match(component, /!values\.modelId/);
|
||||
assert.match(component, /!values\.label/);
|
||||
assert.match(component, /未发现可用模型,请检查供应商配置后重试/);
|
||||
assert.doesNotMatch(component, /可继续手工输入/);
|
||||
assert.match(component, /name="providerModel" label="模型"[\s\S]*<Select[\s\S]*showSearch[\s\S]*optionFilterProp="label"/);
|
||||
assert.doesNotMatch(component, /供应商模型名(可手工输入)|name="modelId"|name="label" label="显示名称"/);
|
||||
assert.match(component, /import \{ createDiscoveredModelId \} from "\.\/model-id"/);
|
||||
assert.match(component, /const modelId = editingModel\?\.modelId \?\? await createDiscoveredModelId\(values\.providerId, values\.providerModel\)/);
|
||||
assert.doesNotMatch(component, /providerModel\.toLowerCase\(\)\.replace/);
|
||||
assert.match(component, /label: editingModel\?\.label \?\? \(discovered\?\.label\?\.trim\(\) \|\| values\.providerModel\)\.slice\(0, 60\)/);
|
||||
});
|
||||
|
||||
test("model form exposes only essential switches and credit cost", () => {
|
||||
assert.match(component, /name="creditCost" label="单次点数"/);
|
||||
assert.match(component, /name="enabled" label="启用"/);
|
||||
assert.match(component, /name="isDefault" label="默认模型"/);
|
||||
for (const field of [
|
||||
"description",
|
||||
"modelTier",
|
||||
"contextWindow",
|
||||
"inputCostMicrousdPerMillion",
|
||||
"outputCostMicrousdPerMillion",
|
||||
"fallbackModelId",
|
||||
"settingsJson",
|
||||
]) {
|
||||
assert.doesNotMatch(component, new RegExp(`name=["']${field}["']`));
|
||||
}
|
||||
assert.doesNotMatch(component, /模型档位|上下文窗口|输入成本(微美元\/百万 Token)|输出成本(微美元\/百万 Token)|回退模型 ID|设置 JSON/);
|
||||
});
|
||||
|
||||
test("hidden model fields preserve draft values or use database-compatible defaults", () => {
|
||||
assert.match(component, /description: editingModel\?\.description \?\? ""/);
|
||||
assert.match(component, /modelTier: editingModel\?\.modelTier \?\? "standard"/);
|
||||
assert.match(component, /contextWindow: editingModel\?\.contextWindow \?\? null/);
|
||||
assert.match(component, /inputCostMicrousdPerMillion: editingModel\?\.inputCostMicrousdPerMillion \?\? 0/);
|
||||
assert.match(component, /outputCostMicrousdPerMillion: editingModel\?\.outputCostMicrousdPerMillion \?\? 0/);
|
||||
assert.match(component, /fallbackModelId: editingModel\?\.fallbackModelId \?\? null/);
|
||||
assert.match(component, /settings: editingModel\?\.settings \?\? \{\}/);
|
||||
});
|
||||
|
||||
test("model mutations omit client reasons while the server keeps fixed audit reasons", () => {
|
||||
|
||||
@@ -215,7 +215,7 @@ test("网关探测固定已验证公网 IP、保留 TLS hostname 且限制重定
|
||||
assert.match(gatewayPolicy, /const resolved = await withinTimeout\([\s\S]*resolvePublicUrl\(value, options\.lookup \?\? defaultLookup\)/);
|
||||
assert.match(gatewayPolicy, /const pinned = resolved\.addresses\[0\]!/);
|
||||
assert.match(gatewayPolicy, /servername: isIP\(hostname\) \? undefined : hostname/);
|
||||
assert.match(gatewayPolicy, /lookup: \(_hostname, _options, callback\) => callback\(null, pinned\.address, pinned\.family\)/);
|
||||
assert.match(gatewayPolicy, /lookup: pinnedAddressLookup\(pinned\)/);
|
||||
assert.match(gatewayPolicy, /requestPinnedHttps\([\s\S]*resolved,[\s\S]*"HEAD"/);
|
||||
assert.match(gatewayPolicy, /headStatus === 405 \|\| headStatus === 501[\s\S]*requestPinnedHttps\([\s\S]*resolved,[\s\S]*"GET"/);
|
||||
assert.match(gatewayPolicy, /status >= 300 && status < 400/);
|
||||
|
||||
@@ -95,7 +95,7 @@ test("model provider URLs allow arbitrary public HTTPS origins but retain SSRF b
|
||||
);
|
||||
|
||||
const gatewayPolicy = readFileSync(new URL("../src/lib/epay/gateway-policy.ts", import.meta.url), "utf8");
|
||||
assert.match(gatewayPolicy, /lookup: \(_hostname, _options, callback\) => callback\(null, pinned\.address, pinned\.family\)/);
|
||||
assert.match(gatewayPolicy, /lookup: pinnedAddressLookup\(pinned\)/);
|
||||
assert.match(gatewayPolicy, /status >= 300 && status < 400/);
|
||||
assert.match(gatewayPolicy, /不允许重定向/);
|
||||
});
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { connect, createServer, type AddressInfo } from "node:net";
|
||||
import { pinnedAddressLookup } from "../src/lib/epay/gateway-policy.ts";
|
||||
import { decryptModelProviderApiKey, encryptModelProviderApiKey, modelProviderModelsUrl, modelProviderRequestHeaders } from "../src/lib/model-provider-policy.ts";
|
||||
|
||||
const key=Buffer.alloc(32,7).toString("base64");
|
||||
test("model provider credentials use the dedicated AES-GCM master key",()=>{const encrypted=encryptModelProviderApiKey("provider-secret",{MODEL_PROVIDER_CONFIG_ENCRYPTION_KEY:key});assert.notEqual(encrypted,"provider-secret");assert.equal(decryptModelProviderApiKey({encryptedApiKey:encrypted},{MODEL_PROVIDER_CONFIG_ENCRYPTION_KEY:key}),"provider-secret");assert.throws(()=>decryptModelProviderApiKey({encryptedApiKey:encrypted},{MODEL_PROVIDER_CONFIG_ENCRYPTION_KEY:Buffer.alloc(32,8).toString("base64")}));});
|
||||
test("Anthropic discovery uses its official endpoint and headers",()=>{assert.equal(modelProviderModelsUrl({providerType:"anthropic",baseUrl:null}),"https://api.anthropic.com/v1/models");assert.deepEqual(modelProviderRequestHeaders("anthropic","secret"),{"x-api-key":"secret","anthropic-version":"2023-06-01"});assert.deepEqual(modelProviderRequestHeaders("openai","secret"),{authorization:"Bearer secret"});});
|
||||
test("runtime and admin APIs do not parse legacy provider credential env vars",()=>{for(const file of ["../src/lib/model-catalog.ts","../src/mastra/model.ts","../src/app/api/admin/models/route.ts"]){const source=readFileSync(new URL(file,import.meta.url),"utf8");assert.doesNotMatch(source,/OPENAI_API_KEY|DEEPSEEK_API_KEY|LLM_API_KEY|secret_ref/);}});
|
||||
test("discovery stays on the DNS-pinned bounded HTTPS helper",()=>{const route=readFileSync(new URL("../src/app/api/admin/models/discover/route.ts",import.meta.url),"utf8");const gateway=readFileSync(new URL("../src/lib/epay/gateway-policy.ts",import.meta.url),"utf8");assert.match(route,/requestAllowedModelProvider/);assert.doesNotMatch(route,/\bfetch\s*\(/);assert.match(gateway,/lookup: \(_hostname, _options, callback\)/);assert.match(gateway,/不允许重定向/);assert.match(route,/maxResponseBytes:256\*1024/);});
|
||||
test("discovery stays on the DNS-pinned bounded HTTPS helper",()=>{const route=readFileSync(new URL("../src/app/api/admin/models/discover/route.ts",import.meta.url),"utf8");const gateway=readFileSync(new URL("../src/lib/epay/gateway-policy.ts",import.meta.url),"utf8");assert.match(route,/requestAllowedModelProvider/);assert.doesNotMatch(route,/\bfetch\s*\(/);assert.match(gateway,/lookup: pinnedAddressLookup\(pinned\)/);assert.match(gateway,/不允许重定向/);assert.match(route,/maxResponseBytes:256\*1024/);});
|
||||
test("DNS pinning supports Node all-address and single-address lookups",async(t)=>{const server=createServer((socket)=>socket.end());await new Promise<void>((resolve,reject)=>{server.once("error",reject);server.listen(0,"127.0.0.1",resolve);});t.after(()=>new Promise<void>((resolve,reject)=>server.close((error)=>error?reject(error):resolve())));const {port}=server.address() as AddressInfo;for(const autoSelectFamily of [true,false])await new Promise<void>((resolve,reject)=>{const socket=connect({host:"provider.example",port,autoSelectFamily,lookup:pinnedAddressLookup({address:"127.0.0.1",family:4})});socket.once("connect",()=>{socket.end();resolve();});socket.once("error",reject);});});
|
||||
|
||||
Reference in New Issue
Block a user