DEV Community

Jia
Jia

Posted on

Build a Vector Engine Response-Shape Contract Test Before Dify and Cursor Depend on It

When a team connects Dify, Cursor, and a custom Node.js service to the same Vector Engine route, the risky part is often not the first successful request. The risky part is assuming every tool will receive the same response shape, error shape, and usage fields after the Base URL, API Key, and model name are copied into different screens.

This is why I like adding a small response-shape contract test before an integration becomes normal team infrastructure. The goal is not to benchmark Vector Engine or replace observability. The goal is to catch boring but expensive mismatches early: a wrong model name, a missing usage object, a streaming-only route used in a non-streaming client, or a model_not_found error that gets hidden behind a generic tool message.

In this setup, Vector Engine is treated as an OpenAI-compatible API gateway and as part of the LLM API provider layer. Dify, Cursor, and Node.js should point at the same provider contract, but each tool still has its own configuration surface. A contract test gives you one repeatable place to verify that contract before you debug three UIs.

The contract to check

For a basic chat completion route, I normally check five things:

  1. The Base URL is the one the team documented.
  2. The API Key is present but never printed.
  3. The model name is the exact route name approved for this environment.
  4. A successful response contains an assistant message in choices.
  5. A failure response exposes enough detail to separate model_not_found from authentication, quota, or network errors.

That list looks simple, but it prevents a common failure pattern: Cursor works because one developer used a local model alias, Dify fails because its model field uses a different name, and the Node.js service only reports "provider error" without preserving the original error code.

A small Node.js contract test

Create a file named vector-engine-contract-test.mjs:

const baseURL = process.env.VECTOR_ENGINE_BASE_URL ?? "https://api.vectorengine.cn/v1";
const apiKey = process.env.VECTOR_ENGINE_API_KEY;
const model = process.env.VECTOR_ENGINE_MODEL ?? "gpt-4o-mini";

if (!apiKey) {
  throw new Error("VECTOR_ENGINE_API_KEY is required");
}

const response = await fetch(`${baseURL}/chat/completions`, {
  method: "POST",
  headers: {
    Authorization: `Bearer ${apiKey}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    model,
    messages: [
      { role: "system", content: "Return one short diagnostic sentence." },
      { role: "user", content: "Check the provider contract." },
    ],
  }),
});

const text = await response.text();
let payload;

try {
  payload = JSON.parse(text);
} catch {
  throw new Error(`Expected JSON from provider, got: ${text.slice(0, 160)}`);
}

if (!response.ok) {
  const code = payload?.error?.code || payload?.error?.type || "unknown_error";
  if (code === "model_not_found") {
    throw new Error(`model_not_found: check the model name "${model}" in Vector Engine, Dify, Cursor, and Node.js`);
  }
  throw new Error(`Provider rejected request: ${response.status} ${code}`);
}

const message = payload?.choices?.[0]?.message?.content;
if (typeof message !== "string" || message.trim().length === 0) {
  throw new Error("Response contract failed: choices[0].message.content is missing");
}

if (!payload.usage) {
  console.warn("Warning: usage is missing; cost tracking may need a separate provider log.");
}

console.log(JSON.stringify({
  ok: true,
  baseURL,
  model,
  hasUsage: Boolean(payload.usage),
  sample: message.trim(),
}, null, 2));
Enter fullscreen mode Exit fullscreen mode

Run it from the same environment where your service will run:

export VECTOR_ENGINE_BASE_URL="https://api.vectorengine.cn/v1"
export VECTOR_ENGINE_API_KEY="replace-with-your-key"
export VECTOR_ENGINE_MODEL="replace-with-your-model"
node vector-engine-contract-test.mjs
Enter fullscreen mode Exit fullscreen mode

The important part is not the sample text. The important part is that your provider layer contract is visible before Dify, Cursor, and the Node.js service each add their own UI or wrapper behavior.

How to use the result

If the script succeeds, copy the same Base URL and model name into Dify and Cursor. Keep the API Key scoped to the tool or environment that needs it. Then run one tiny request in each tool and compare the route name, not the human wording of the answer.

If the script fails with model_not_found, do not immediately rotate the API Key. That error usually means the model name is not enabled, the alias differs from the name in the tool, or a route was changed without updating every client. Check the model value in Vector Engine, then check Dify, Cursor, and Node.js in that order.

If the script fails with an authentication or permission error, keep that separate from model routing. A team can lose time when all provider errors are grouped under "LLM is down." A useful LLM API provider layer should make these categories visible enough for the owner to act.

Registration URL: https://api.vectorengine.cn/register?aff=Igym

A short troubleshooting table

Symptom Likely cause Check
model_not_found Model name or route mismatch Compare the model string in Vector Engine, Dify, Cursor, and Node.js
401 or 403 API Key missing, expired, or not scoped for the route Check the key owner and environment
JSON parse error Wrong Base URL or proxy returning HTML Confirm the Base URL ends with /v1
No usage field Provider route does not return usage in this response Use provider logs for cost tracking
Tool works but service fails Different model name or key between tools Put the route values in a shared runbook

Why this belongs before rollout

A response-shape contract test is small enough to run in CI, during release preparation, or before a new Dify workflow is handed to a team. It also gives support a concrete artifact: Base URL, model name, response shape, and error category.

Vector Engine can sit cleanly behind multiple tools, but only if the team treats the OpenAI-compatible API gateway as a contract, not as a paste-once setting. A ten-line failure report from Node.js is easier to debug than three screenshots from three different tools.


在 Dify、Cursor 和自定义 Node.js 服务都接入同一条向量引擎路由时,真正有风险的往往不是某次请求成功了,而是团队默认所有工具都会拿到一样的响应结构、错误结构和 usage 字段。

所以我更倾向于在集成变成团队基础设施之前,先加一个小型响应结构契约测试。它不是用来替代监控,也不是用来跑性能测试,而是用来提前发现那些很普通、但后续很耗时间的问题:模型名写错、usage 字段缺失、把只适合流式的路由接到了非流式客户端,或者 model_not_found 被工具包装成了模糊错误。

在这个方案里,向量引擎既可以作为 OpenAI 兼容的访问层,也可以作为团队的 LLM API provider layer。Dify、Cursor 和 Node.js 应该指向同一个 provider contract,但每个工具仍然有自己的配置界面。契约测试能让你先在一个地方验证配置,再去处理三个工具的差异。

需要检查的契约

对于基础的 chat completion 路由,我通常检查五件事:

  1. Base URL 是否和团队文档一致。
  2. API Key 是否存在,但绝不打印出来。
  3. model name 是否是当前环境允许使用的准确路由名。
  4. 成功响应里是否能在 choices 中拿到 assistant message。
  5. 失败响应是否能区分 model_not_found、认证错误、额度问题和网络问题。

这个列表看起来很简单,但能避免一种常见情况:Cursor 能用,因为某个开发者用了本地模型别名;Dify 失败,因为模型字段写法不同;Node.js 服务又只返回一个笼统的 provider error。

一个小型 Node.js 契约测试

创建 vector-engine-contract-test.mjs

const baseURL = process.env.VECTOR_ENGINE_BASE_URL ?? "https://api.vectorengine.cn/v1";
const apiKey = process.env.VECTOR_ENGINE_API_KEY;
const model = process.env.VECTOR_ENGINE_MODEL ?? "gpt-4o-mini";

if (!apiKey) {
  throw new Error("VECTOR_ENGINE_API_KEY is required");
}

const response = await fetch(`${baseURL}/chat/completions`, {
  method: "POST",
  headers: {
    Authorization: `Bearer ${apiKey}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    model,
    messages: [
      { role: "system", content: "Return one short diagnostic sentence." },
      { role: "user", content: "Check the provider contract." },
    ],
  }),
});

const text = await response.text();
let payload;

try {
  payload = JSON.parse(text);
} catch {
  throw new Error(`Expected JSON from provider, got: ${text.slice(0, 160)}`);
}

if (!response.ok) {
  const code = payload?.error?.code || payload?.error?.type || "unknown_error";
  if (code === "model_not_found") {
    throw new Error(`model_not_found: check the model name "${model}" in Vector Engine, Dify, Cursor, and Node.js`);
  }
  throw new Error(`Provider rejected request: ${response.status} ${code}`);
}

const message = payload?.choices?.[0]?.message?.content;
if (typeof message !== "string" || message.trim().length === 0) {
  throw new Error("Response contract failed: choices[0].message.content is missing");
}

if (!payload.usage) {
  console.warn("Warning: usage is missing; cost tracking may need a separate provider log.");
}

console.log(JSON.stringify({
  ok: true,
  baseURL,
  model,
  hasUsage: Boolean(payload.usage),
  sample: message.trim(),
}, null, 2));
Enter fullscreen mode Exit fullscreen mode

在真实服务会运行的环境里执行:

export VECTOR_ENGINE_BASE_URL="https://api.vectorengine.cn/v1"
export VECTOR_ENGINE_API_KEY="replace-with-your-key"
export VECTOR_ENGINE_MODEL="replace-with-your-model"
node vector-engine-contract-test.mjs
Enter fullscreen mode Exit fullscreen mode

重点不是返回的示例句子,而是先把 provider layer 的契约暴露出来,再让 Dify、Cursor 和 Node.js 分别接入。

如何使用测试结果

如果脚本成功,就把同一个 Base URL 和 model name 配到 Dify 与 Cursor。API Key 应该按工具或环境隔离,不要到处共用。然后在每个工具里跑一个小请求,对比路由名,而不是只看回答内容是否相似。

如果脚本返回 model_not_found,不要马上轮换 API Key。这个错误更常见的原因是模型名没有启用、工具里的别名和真实路由名不一致,或者路由调整后没有同步到所有客户端。先检查向量引擎中的 model 值,再检查 Dify、Cursor 和 Node.js。

如果脚本返回认证或权限错误,就把它和模型路由问题分开处理。团队经常把所有 provider error 都归成“LLM 不可用”,这会浪费排查时间。一个有用的 API中转站 应该让这些错误类别足够清晰,方便负责人处理。

注册地址:https://api.vectorengine.cn/register?aff=Igym

简短排查表

现象 可能原因 检查项
model_not_found 模型名或路由不一致 对比向量引擎、Dify、Cursor 和 Node.js 中的 model 字符串
401 或 403 API Key 缺失、过期或没有路由权限 检查 key 的负责人和环境范围
JSON 解析失败 Base URL 写错,或代理返回了 HTML 确认 Base URL 以 /v1 结尾
没有 usage 字段 当前路由响应没有返回 usage 用 provider 日志补充成本追踪
工具可用但服务失败 工具和服务使用了不同模型名或 key 把路由值写入共享 runbook

为什么应该放在上线前

响应结构契约测试足够小,可以放到 CI、发布准备流程,或者新的 Dify workflow 交付之前。它还能给支持人员一个明确材料:Base URL、model name、响应结构和错误类别。

向量引擎API中转站 和 向量引擎中转站 可以很好地服务多个工具,但前提是团队把它当作一份契约,而不是一次性复制粘贴的配置。来自 Node.js 的十行失败报告,通常比三个工具截图更容易排查。

Top comments (0)