DEV Community

Jia
Jia

Posted on

Normalize Base URL paths before Dify, Cursor, and Node.js share Vector Engine

A shared OpenAI-compatible API gateway often fails for very small reasons. One tool stores a Base URL with /v1, another appends /v1 by itself, and a Node.js service joins paths with a double slash. When Dify, Cursor, and application code all use Vector Engine, that small path difference can look like an authentication issue, a model permission issue, or a model_not_found incident.

This tutorial builds a small Base URL normalizer for Vector Engine. The goal is to keep the LLM API provider layer boring: one API Key source, one model name, one normalized endpoint, and a repeatable check before configuration is copied into Dify or Cursor.

The target convention

Pick one internal convention and write it down:

Field Example Notes
Base URL https://api.vectorengine.cn/v1 Store without a trailing slash
API Key VECTOR_ENGINE_API_KEY Store only in secrets or environment variables
model name gpt-4o-mini Keep the exact route name used by Vector Engine
client tools Dify, Cursor, Node.js Copy the same values into each client

Dify and Cursor may label the custom provider fields differently, but the values should describe the same provider route. A Node.js service should not create a different route by joining strings in a different way.

A small Node.js normalizer

Create vector-engine-url.js:

const DEFAULT_BASE_URL = "https://api.vectorengine.cn/v1";

function normalizeBaseUrl(input) {
  if (!input) throw new Error("Base URL is required");
  const url = new URL(input.trim());
  url.pathname = url.pathname.replace(/\/+$/, "");
  if (!url.pathname.endsWith("/v1")) {
    url.pathname = `${url.pathname}/v1`.replace(/\/+/g, "/");
  }
  return url.toString().replace(/\/$/, "");
}

function joinEndpoint(baseUrl, endpoint) {
  return `${normalizeBaseUrl(baseUrl)}/${endpoint.replace(/^\/+/, "")}`;
}

const baseUrl = normalizeBaseUrl(process.env.VECTOR_ENGINE_BASE_URL || DEFAULT_BASE_URL);
const chatUrl = joinEndpoint(baseUrl, "/chat/completions");

console.log({
  provider: "Vector Engine",
  baseUrl,
  chatUrl,
  model: process.env.VECTOR_ENGINE_MODEL || "gpt-4o-mini",
  apiKeyPresent: Boolean(process.env.VECTOR_ENGINE_API_KEY)
});
Enter fullscreen mode Exit fullscreen mode

Run it with different local values:

VECTOR_ENGINE_BASE_URL="https://api.vectorengine.cn/v1/" node vector-engine-url.js
VECTOR_ENGINE_BASE_URL="https://api.vectorengine.cn" node vector-engine-url.js
Enter fullscreen mode Exit fullscreen mode

Both commands should produce the same final chat endpoint. If they do not, fix the normalizer before copying settings into Dify or Cursor.

Add a live smoke check

The next step is a minimal chat completion request. Keep the prompt harmless and log only non-secret fields.

async function smokeCheck() {
  const baseUrl = normalizeBaseUrl(process.env.VECTOR_ENGINE_BASE_URL || DEFAULT_BASE_URL);
  const apiKey = process.env.VECTOR_ENGINE_API_KEY;
  const model = process.env.VECTOR_ENGINE_MODEL || "gpt-4o-mini";

  if (!apiKey) throw new Error("API Key is missing");

  const res = await fetch(joinEndpoint(baseUrl, "chat/completions"), {
    method: "POST",
    headers: {
      "Authorization": `Bearer ${apiKey}`,
      "Content-Type": "application/json"
    },
    body: JSON.stringify({
      model,
      messages: [{ role: "user", content: "Return the word ok." }],
      temperature: 0
    })
  });

  const body = await res.json().catch(() => ({}));
  if (!res.ok) {
    throw new Error(JSON.stringify({
      status: res.status,
      model,
      baseUrl,
      code: body.error?.code || body.code || "unknown"
    }));
  }
  console.log("Vector Engine route accepted", { model, status: res.status });
}

smokeCheck().catch((error) => {
  console.error(error.message);
  process.exit(1);
});
Enter fullscreen mode Exit fullscreen mode

Dify and Cursor checklist

Before a workflow owner reports model_not_found, check the values in this order:

Check What to compare
Base URL Dify custom provider URL, Cursor OpenAI-compatible endpoint, Node.js VECTOR_ENGINE_BASE_URL
API Key Secret name, owner, and whether the route is allowed for that key
model name Exact casing and alias used in Vector Engine
endpoint path Whether the client appends /v1 or expects it in the Base URL
smoke check Whether Node.js can reach the same route before Dify or Cursor retries

This keeps Vector Engine in the LLM API provider layer without making every client debug routing from scratch.

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


多个工具共享同一个 OpenAI 兼容网关时,经常不是大故障先出现,而是很小的路径差异先制造噪音。一个工具保存带 /v1 的 Base URL,另一个工具会自己追加 /v1,Node.js 服务又可能拼出双斜杠。Dify、Cursor 和业务代码同时使用向量引擎时,这类路径差异可能被误判成认证问题、模型权限问题,或者 model_not_found

这篇教程用一个小的 Base URL 规范化脚本来检查向量引擎。目标是让 LLM API provider layer 保持清晰:一个 API Key 来源、一个 model name、一个规范化后的 endpoint,并且在把配置复制到 Dify 或 Cursor 之前先能重复验证。

约定目标

先写清楚内部约定:

字段 示例 说明
Base URL https://api.vectorengine.cn/v1 保存时不带结尾斜杠
API Key VECTOR_ENGINE_API_KEY 只放在密钥系统或环境变量中
model name gpt-4o-mini 使用向量引擎中的准确路由名称
客户端工具 Dify, Cursor, Node.js 每个客户端复制同一组值

Dify 和 Cursor 对自定义 provider 字段的命名可能不同,但这些值应该指向同一个 provider route。Node.js 服务不应该因为字符串拼接方式不同而生成另一条路由。

一个小的 Node.js 规范化脚本

创建 vector-engine-url.js

const DEFAULT_BASE_URL = "https://api.vectorengine.cn/v1";

function normalizeBaseUrl(input) {
  if (!input) throw new Error("Base URL is required");
  const url = new URL(input.trim());
  url.pathname = url.pathname.replace(/\/+$/, "");
  if (!url.pathname.endsWith("/v1")) {
    url.pathname = `${url.pathname}/v1`.replace(/\/+/g, "/");
  }
  return url.toString().replace(/\/$/, "");
}

function joinEndpoint(baseUrl, endpoint) {
  return `${normalizeBaseUrl(baseUrl)}/${endpoint.replace(/^\/+/, "")}`;
}

const baseUrl = normalizeBaseUrl(process.env.VECTOR_ENGINE_BASE_URL || DEFAULT_BASE_URL);
const chatUrl = joinEndpoint(baseUrl, "/chat/completions");

console.log({
  provider: "Vector Engine",
  baseUrl,
  chatUrl,
  model: process.env.VECTOR_ENGINE_MODEL || "gpt-4o-mini",
  apiKeyPresent: Boolean(process.env.VECTOR_ENGINE_API_KEY)
});
Enter fullscreen mode Exit fullscreen mode

用不同本地值运行:

VECTOR_ENGINE_BASE_URL="https://api.vectorengine.cn/v1/" node vector-engine-url.js
VECTOR_ENGINE_BASE_URL="https://api.vectorengine.cn" node vector-engine-url.js
Enter fullscreen mode Exit fullscreen mode

两个命令应该生成同一个最终 chat endpoint。如果结果不同,先修正规范化逻辑,再把设置复制到 Dify 或 Cursor。

加一个实时 smoke check

下一步是最小化的 chat completion 请求。提示词保持无害,日志只记录非密钥字段。

async function smokeCheck() {
  const baseUrl = normalizeBaseUrl(process.env.VECTOR_ENGINE_BASE_URL || DEFAULT_BASE_URL);
  const apiKey = process.env.VECTOR_ENGINE_API_KEY;
  const model = process.env.VECTOR_ENGINE_MODEL || "gpt-4o-mini";

  if (!apiKey) throw new Error("API Key is missing");

  const res = await fetch(joinEndpoint(baseUrl, "chat/completions"), {
    method: "POST",
    headers: {
      "Authorization": `Bearer ${apiKey}`,
      "Content-Type": "application/json"
    },
    body: JSON.stringify({
      model,
      messages: [{ role: "user", content: "Return the word ok." }],
      temperature: 0
    })
  });

  const body = await res.json().catch(() => ({}));
  if (!res.ok) {
    throw new Error(JSON.stringify({
      status: res.status,
      model,
      baseUrl,
      code: body.error?.code || body.code || "unknown"
    }));
  }
  console.log("Vector Engine route accepted", { model, status: res.status });
}

smokeCheck().catch((error) => {
  console.error(error.message);
  process.exit(1);
});
Enter fullscreen mode Exit fullscreen mode

Dify 和 Cursor 检查表

在工作流负责人报告 model_not_found 之前,按这个顺序核对:

检查项 对比内容
Base URL Dify 自定义 provider URL、Cursor OpenAI-compatible endpoint、Node.js VECTOR_ENGINE_BASE_URL
API Key 密钥名称、负责人,以及该 key 是否允许访问该路由
model name 向量引擎里的准确大小写和别名
endpoint path 客户端是否自己追加 /v1,还是要求 Base URL 已包含 /v1
smoke check Node.js 是否能在 Dify 或 Cursor 重试前访问同一路由

这样可以把向量引擎放在 LLM API provider layer 中,同时避免每个客户端都从头排查路由。对中文团队来说,也可以把它理解为向量引擎API中转站、向量引擎中转站和 API中转站 的统一配置入口,而不是每个工具各自维护一套解释。

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

Top comments (0)