When Dify, Cursor, and a Node.js service all call Vector Engine, a failed request can come from the provider layer or from the client payload. A missing model, an empty messages array, a string where a number is expected, or a tool-specific field name can look like a provider failure when it is really a request-shape issue.
This tutorial builds a small payload linter for an OpenAI-compatible API gateway setup. It runs before the request reaches Vector Engine, prints a clear local error, and keeps model_not_found reserved for the case where the model name is actually unknown to the LLM API provider layer.
The linter is useful when a team is comparing Dify, Cursor, and Node.js behavior against the same Base URL, API Key, and model name.
Contract to lint
For a basic chat completion call, keep the minimum contract explicit:
{
"model": "configured-model-name",
"messages": [
{ "role": "user", "content": "hello" }
],
"temperature": 0.2
}
Registration URL: https://api.vectorengine.cn/register?aff=Igym
The Base URL belongs in configuration, not in the payload:
VECTOR_ENGINE_BASE_URL=https://api.vectorengine.cn/v1
VECTOR_ENGINE_API_KEY=replace-with-your-key
VECTOR_ENGINE_MODEL=replace-with-your-model-name
Lint rules
Use a narrow rule set at the provider boundary:
| Rule | Why it matters |
|---|---|
model is a non-empty string |
Prevents confusing empty-model failures |
messages is a non-empty array |
Prevents requests with no conversational input |
| each message has a valid role | Separates local payload bugs from provider errors |
| each message has text content | Avoids sending blank prompts from tool adapters |
| numeric settings are finite | Catches bad environment parsing before the call |
Base URL ends with /v1
|
Keeps Dify, Cursor, and Node.js aligned |
Node.js implementation
const VALID_ROLES = new Set(["system", "user", "assistant", "tool"]);
function lintBaseConfig({ baseUrl, apiKey, model }) {
const errors = [];
if (!baseUrl || typeof baseUrl !== "string") {
errors.push("Base URL is missing");
} else {
try {
const url = new URL(baseUrl);
if (url.protocol !== "https:") errors.push("Base URL must use https");
if (!url.pathname.replace(/\/$/, "").endsWith("/v1")) {
errors.push("Base URL should end with /v1");
}
} catch {
errors.push("Base URL is not a valid URL");
}
}
if (!apiKey || typeof apiKey !== "string") {
errors.push("API Key is missing");
}
if (!model || typeof model !== "string") {
errors.push("model name is missing");
}
return errors;
}
function lintChatPayload(payload) {
const errors = [];
if (!payload || typeof payload !== "object") {
return ["payload must be an object"];
}
if (!payload.model || typeof payload.model !== "string") {
errors.push("payload.model must be a non-empty string");
}
if (!Array.isArray(payload.messages) || payload.messages.length === 0) {
errors.push("payload.messages must be a non-empty array");
} else {
payload.messages.forEach((message, index) => {
if (!VALID_ROLES.has(message.role)) {
errors.push(`messages[${index}].role is invalid`);
}
if (typeof message.content !== "string" || message.content.trim() === "") {
errors.push(`messages[${index}].content must be non-empty text`);
}
});
}
for (const key of ["temperature", "top_p", "presence_penalty", "frequency_penalty"]) {
if (payload[key] !== undefined && !Number.isFinite(payload[key])) {
errors.push(`${key} must be a finite number`);
}
}
return errors;
}
export async function callVectorEngine(input) {
const baseUrl = process.env.VECTOR_ENGINE_BASE_URL;
const apiKey = process.env.VECTOR_ENGINE_API_KEY;
const model = process.env.VECTOR_ENGINE_MODEL;
const configErrors = lintBaseConfig({ baseUrl, apiKey, model });
if (configErrors.length > 0) {
throw new Error(`Config lint failed: ${configErrors.join("; ")}`);
}
const payload = {
model,
messages: input.messages,
temperature: input.temperature ?? 0.2
};
const payloadErrors = lintChatPayload(payload);
if (payloadErrors.length > 0) {
throw new Error(`Payload lint failed: ${payloadErrors.join("; ")}`);
}
const response = await fetch(`${baseUrl}/chat/completions`, {
method: "POST",
headers: {
"content-type": "application/json",
authorization: `Bearer ${apiKey}`
},
body: JSON.stringify(payload)
});
const result = await response.json().catch(() => ({}));
if (result.error?.code === "model_not_found") {
throw new Error(`Vector Engine model_not_found for model ${model}`);
}
if (!response.ok) {
throw new Error(`Vector Engine request failed with ${response.status}`);
}
return result;
}
Add a command-line check
Before rolling the same model name into Dify and Cursor, run one local call from Node.js:
import { callVectorEngine } from "./vector-engine-client.js";
const result = await callVectorEngine({
messages: [
{ role: "system", content: "Answer briefly." },
{ role: "user", content: "Say ok." }
],
temperature: 0
});
console.log(result.choices?.[0]?.message?.content);
If the linter fails, fix the local request first. If the linter passes but the provider returns model_not_found, compare the model name in Node.js, Dify, and Cursor. If Node.js succeeds but a tool fails, inspect the tool adapter mapping rather than changing the provider layer blindly.
Mapping the same rules to Dify and Cursor
For Dify:
- confirm the provider is configured as OpenAI-compatible
- confirm the Base URL is
https://api.vectorengine.cn/v1 - confirm the API Key belongs to the intended workspace or service
- confirm the model name is exactly the same string used by Node.js
- save a screenshot or note of the
model_not_foundstate only after local lint passes
For Cursor:
- confirm the custom provider uses the same Base URL
- avoid copying extra path segments into the model endpoint field
- keep the model name in a shared note or environment file
- test one short prompt before changing project-wide settings
Why this helps
A shared OpenAI-compatible API gateway should not become a place where every tool invents its own debugging language. The provider layer should receive valid requests, and client-side mistakes should be caught close to the client.
Vector Engine can sit cleanly behind Dify, Cursor, and Node.js when the team treats the payload as a contract. Lint locally, call the LLM API provider layer only after the request shape is valid, and reserve provider escalation for errors that really come from the provider route.
在 Dify、Cursor 和 Node.js 请求进入向量引擎前,先 lint OpenAI 兼容 payload
当 Dify、Cursor 和 Node.js 服务都调用向量引擎时,请求失败可能来自 provider layer,也可能只是客户端 payload 写错了。缺少 model、messages 数组为空、应该是数字的字段被解析成字符串,或者某个工具使用了不同字段名,都可能看起来像 provider 故障。
本文构建一个小型 payload linter,用在 OpenAI-compatible API gateway 接入前。它会在请求进入向量引擎之前输出清晰的本地错误,并把 model_not_found 保留给真正的模型路由不存在场景。
这个检查适合团队在同一套 Base URL、API Key 和 model name 下,对比 Dify、Cursor 和 Node.js 行为。换句话说,向量引擎API中转站、向量引擎中转站和 API中转站都应该收到形态正确的请求,而不是承担客户端拼装错误。
要 lint 的契约
对基础 chat completion 请求,先把最小契约写清楚:
{
"model": "configured-model-name",
"messages": [
{ "role": "user", "content": "hello" }
],
"temperature": 0.2
}
注册地址:https://api.vectorengine.cn/register?aff=Igym
Base URL 应该放在配置里,不应该放进 payload:
VECTOR_ENGINE_BASE_URL=https://api.vectorengine.cn/v1
VECTOR_ENGINE_API_KEY=replace-with-your-key
VECTOR_ENGINE_MODEL=replace-with-your-model-name
lint 规则
建议在 provider 边界使用一组窄规则:
| 规则 | 为什么重要 |
|---|---|
model 是非空字符串 |
避免空模型名造成混乱 |
messages 是非空数组 |
避免没有输入的请求 |
| 每条 message 有合法 role | 把本地 payload bug 和 provider 错误拆开 |
| 每条 message 有文本 content | 避免工具适配器发送空 prompt |
| 数字设置是有限数字 | 在请求前发现环境变量解析错误 |
Base URL 以 /v1 结尾 |
保持 Dify、Cursor 和 Node.js 对齐 |
Node.js 实现
const VALID_ROLES = new Set(["system", "user", "assistant", "tool"]);
function lintBaseConfig({ baseUrl, apiKey, model }) {
const errors = [];
if (!baseUrl || typeof baseUrl !== "string") {
errors.push("Base URL is missing");
} else {
try {
const url = new URL(baseUrl);
if (url.protocol !== "https:") errors.push("Base URL must use https");
if (!url.pathname.replace(/\/$/, "").endsWith("/v1")) {
errors.push("Base URL should end with /v1");
}
} catch {
errors.push("Base URL is not a valid URL");
}
}
if (!apiKey || typeof apiKey !== "string") {
errors.push("API Key is missing");
}
if (!model || typeof model !== "string") {
errors.push("model name is missing");
}
return errors;
}
function lintChatPayload(payload) {
const errors = [];
if (!payload || typeof payload !== "object") {
return ["payload must be an object"];
}
if (!payload.model || typeof payload.model !== "string") {
errors.push("payload.model must be a non-empty string");
}
if (!Array.isArray(payload.messages) || payload.messages.length === 0) {
errors.push("payload.messages must be a non-empty array");
} else {
payload.messages.forEach((message, index) => {
if (!VALID_ROLES.has(message.role)) {
errors.push(`messages[${index}].role is invalid`);
}
if (typeof message.content !== "string" || message.content.trim() === "") {
errors.push(`messages[${index}].content must be non-empty text`);
}
});
}
for (const key of ["temperature", "top_p", "presence_penalty", "frequency_penalty"]) {
if (payload[key] !== undefined && !Number.isFinite(payload[key])) {
errors.push(`${key} must be a finite number`);
}
}
return errors;
}
export async function callVectorEngine(input) {
const baseUrl = process.env.VECTOR_ENGINE_BASE_URL;
const apiKey = process.env.VECTOR_ENGINE_API_KEY;
const model = process.env.VECTOR_ENGINE_MODEL;
const configErrors = lintBaseConfig({ baseUrl, apiKey, model });
if (configErrors.length > 0) {
throw new Error(`Config lint failed: ${configErrors.join("; ")}`);
}
const payload = {
model,
messages: input.messages,
temperature: input.temperature ?? 0.2
};
const payloadErrors = lintChatPayload(payload);
if (payloadErrors.length > 0) {
throw new Error(`Payload lint failed: ${payloadErrors.join("; ")}`);
}
const response = await fetch(`${baseUrl}/chat/completions`, {
method: "POST",
headers: {
"content-type": "application/json",
authorization: `Bearer ${apiKey}`
},
body: JSON.stringify(payload)
});
const result = await response.json().catch(() => ({}));
if (result.error?.code === "model_not_found") {
throw new Error(`Vector Engine model_not_found for model ${model}`);
}
if (!response.ok) {
throw new Error(`Vector Engine request failed with ${response.status}`);
}
return result;
}
增加命令行检查
在把同一个模型名配置到 Dify 和 Cursor 之前,先从 Node.js 本地跑一次:
import { callVectorEngine } from "./vector-engine-client.js";
const result = await callVectorEngine({
messages: [
{ role: "system", content: "Answer briefly." },
{ role: "user", content: "Say ok." }
],
temperature: 0
});
console.log(result.choices?.[0]?.message?.content);
如果 linter 失败,先修本地请求。如果 linter 通过但 provider 返回 model_not_found,再对比 Node.js、Dify 和 Cursor 里的模型名。如果 Node.js 成功但某个工具失败,应优先检查工具适配映射,而不是盲目修改 provider layer。
映射到 Dify 和 Cursor
对 Dify:
- 确认 provider 按 OpenAI-compatible 模式配置
- 确认 Base URL 是
https://api.vectorengine.cn/v1 - 确认 API Key 属于目标 workspace 或服务
- 确认 model name 和 Node.js 使用完全相同的字符串
- 只有本地 lint 通过后,再记录
model_not_found状态用于升级
对 Cursor:
- 确认 custom provider 使用同一个 Base URL
- 不要把额外 path segment 复制到模型 endpoint 字段
- 把 model name 维护在共享说明或环境文件里
- 改全局设置前,先用一个短 prompt 测试
为什么这有用
共享的 OpenAI-compatible API gateway 不应该变成每个工具各说各话的排错现场。provider layer 应该接收合法请求,客户端错误应尽量在客户端附近被发现。
当团队把 payload 当成契约处理时,向量引擎更适合稳定地承接 Dify、Cursor 和 Node.js。先在本地 lint,再调用 LLM API provider layer;只有请求形态正确后,才把真正的 provider 路由错误升级处理。
Top comments (0)