DEV Community

Jia
Jia

Posted on

Lint OpenAI-Compatible Chat Payloads Before They Reach Vector Engine from Dify, Cursor, and Node.js

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
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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_found state 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 写错了。缺少 modelmessages 数组为空、应该是数字的字段被解析成字符串,或者某个工具使用了不同字段名,都可能看起来像 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
}
Enter fullscreen mode Exit fullscreen mode

注册地址: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
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

增加命令行检查

在把同一个模型名配置到 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);
Enter fullscreen mode Exit fullscreen mode

如果 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)