@agentproto/llm-endpoint 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -150,11 +150,12 @@ configured from a JSON file instead of a pair of env vars per server:
150
150
  "defaultRequestFields": { "chat_template_kwargs": { "enable_thinking": false } },
151
151
  "timeoutMs": { "firstTokenMs": 180000 }
152
152
  },
153
- { "id": "ollama", "kind": "openai", "baseUrl": "http://192.168.1.20:11434/v1" },
153
+ { "id": "ollama", "kind": "openai", "baseUrl": "http://192.168.1.20:11434/v1", "connector": "ollama" },
154
154
  {
155
155
  "id": "lmstudio",
156
156
  "kind": "openai",
157
157
  "baseUrl": "http://127.0.0.1:1234/v1",
158
+ "connector": "lmstudio",
158
159
  "defaultRequestFields": { "reasoning_effort": "none" }
159
160
  }
160
161
  ]
@@ -172,6 +173,13 @@ a 500 for the whole listing). `forge` itself keeps working unchanged — it's
172
173
  the implicit endpoint that `FORGE_BASE_URL`/`FORGE_API_KEY` configure; a file
173
174
  entry may not reuse the id `"forge"`.
174
175
 
176
+ - **`connector`** names which local/LAN runtime `baseUrl` points at —
177
+ `lmstudio`, `ollama`, `vllm`, `llama-server`, or the generic
178
+ `openai-compatible` fallback (see `src/connectors.ts`). Optional: it never
179
+ affects request routing, only which connector's `listModels`/request quirks
180
+ `agentproto llm endpoints test`/`detect`/`doctor` use to show loaded models
181
+ and context size. An entry with no `connector` is treated as
182
+ `openai-compatible`.
175
183
  - **`apiKeyEnv`** names an env var (never the key itself). Absent, or the env
176
184
  var unset, means the endpoint is always keyless — no `Authorization` header
177
185
  sent, same as `forge` with no `FORGE_API_KEY`.
package/dist/cli.mjs CHANGED
@@ -575,28 +575,28 @@ function flattenContent(content) {
575
575
  return content.map((c) => c.text).join("");
576
576
  }
577
577
  function translateInputToMessages(input, instructions) {
578
- const messages = [];
579
- if (instructions) {
580
- messages.push({ role: "system", content: instructions });
581
- }
578
+ const sysParts = [];
579
+ if (instructions) sysParts.push(instructions);
580
+ const rest = [];
582
581
  if (typeof input === "string") {
583
- messages.push({ role: "user", content: input });
584
- return messages;
585
- }
586
- for (const item of input) {
587
- if (item.type === "message") {
588
- messages.push({
589
- role: item.role === "developer" ? "system" : item.role,
590
- content: flattenContent(item.content)
591
- });
592
- } else if (item.type === "function_call_output") {
593
- messages.push({
594
- role: "tool",
595
- tool_call_id: item.call_id,
596
- content: item.output
597
- });
582
+ rest.push({ role: "user", content: input });
583
+ } else {
584
+ for (const item of input) {
585
+ if (item.type === "message") {
586
+ if (item.role === "developer" || item.role === "system") {
587
+ const text = flattenContent(item.content);
588
+ if (text) sysParts.push(text);
589
+ continue;
590
+ }
591
+ rest.push({ role: item.role, content: flattenContent(item.content) });
592
+ } else if (item.type === "function_call_output") {
593
+ rest.push({ role: "tool", tool_call_id: item.call_id, content: item.output });
594
+ }
598
595
  }
599
596
  }
597
+ const messages = [];
598
+ if (sysParts.length) messages.push({ role: "system", content: sysParts.join("\n\n") });
599
+ messages.push(...rest);
600
600
  return messages;
601
601
  }
602
602
  function translateToolToChatCompletions(tool) {
@@ -1536,6 +1536,14 @@ async function resumeIncompleteLocalQueueBatches() {
1536
1536
  }
1537
1537
  }
1538
1538
  }
1539
+
1540
+ // src/connectors.ts
1541
+ var CONNECTOR_IDS = ["lmstudio", "ollama", "vllm", "llama-server", "openai-compatible"];
1542
+ function isConnectorId(value) {
1543
+ return typeof value === "string" && CONNECTOR_IDS.includes(value);
1544
+ }
1545
+
1546
+ // src/endpoints.ts
1539
1547
  var FORBIDDEN_DEFAULT_REQUEST_FIELD_KEYS = /* @__PURE__ */ new Set(["model", "messages", "stream", "tools", "input"]);
1540
1548
  var RESERVED_ENDPOINT_IDS = /* @__PURE__ */ new Set(["forge"]);
1541
1549
  function validateEndpointConfig(raw, where, errors) {
@@ -1543,7 +1551,7 @@ function validateEndpointConfig(raw, where, errors) {
1543
1551
  errors.push(`${where}: expected an object, got ${raw === null ? "null" : typeof raw}`);
1544
1552
  return null;
1545
1553
  }
1546
- const { id, kind, baseUrl, apiKeyEnv, defaultRequestFields, timeoutMs } = raw;
1554
+ const { id, kind, baseUrl, connector, apiKeyEnv, defaultRequestFields, timeoutMs } = raw;
1547
1555
  let ok = true;
1548
1556
  if (typeof id !== "string" || id.length === 0) {
1549
1557
  errors.push(`${where}.id: required non-empty string`);
@@ -1571,6 +1579,10 @@ function validateEndpointConfig(raw, where, errors) {
1571
1579
  ok = false;
1572
1580
  }
1573
1581
  }
1582
+ if (connector !== void 0 && !isConnectorId(connector)) {
1583
+ errors.push(`${where}.connector: must be one of ${CONNECTOR_IDS.join(", ")} when present (got ${JSON.stringify(connector)})`);
1584
+ ok = false;
1585
+ }
1574
1586
  if (apiKeyEnv !== void 0 && (typeof apiKeyEnv !== "string" || apiKeyEnv.length === 0)) {
1575
1587
  errors.push(`${where}.apiKeyEnv: must be a non-empty string when present`);
1576
1588
  ok = false;
@@ -1607,6 +1619,7 @@ function validateEndpointConfig(raw, where, errors) {
1607
1619
  }
1608
1620
  if (!ok || typeof id !== "string" || typeof baseUrl !== "string" || !validUrl) return null;
1609
1621
  const built = { id, kind: "openai", baseUrl };
1622
+ if (isConnectorId(connector)) built.connector = connector;
1610
1623
  if (typeof apiKeyEnv === "string") built.apiKeyEnv = apiKeyEnv;
1611
1624
  if (builtFields) built.defaultRequestFields = builtFields;
1612
1625
  if (builtTimeout) built.timeoutMs = builtTimeout;
@@ -2558,21 +2571,28 @@ function handleChatCompletionsRequest(req, res, opts) {
2558
2571
  });
2559
2572
  }
2560
2573
  function adaptAnthropicToOpenAI(payload) {
2574
+ const sysParts = [];
2561
2575
  if (payload.system != null) {
2562
- let sysText = "";
2563
2576
  if (typeof payload.system === "string") {
2564
- sysText = payload.system;
2577
+ if (payload.system) sysParts.push(payload.system);
2565
2578
  } else if (Array.isArray(payload.system)) {
2566
- sysText = payload.system.map((b) => typeof b === "string" ? b : b?.text ?? "").filter(Boolean).join("\n\n");
2567
- }
2568
- if (sysText) {
2569
- if (!Array.isArray(payload.messages)) payload.messages = [];
2570
- if (!payload.messages[0] || payload.messages[0].role !== "system") {
2571
- payload.messages.unshift({ role: "system", content: sysText });
2572
- }
2579
+ const text = payload.system.map((b) => typeof b === "string" ? b : b?.text ?? "").filter(Boolean).join("\n\n");
2580
+ if (text) sysParts.push(text);
2573
2581
  }
2574
2582
  delete payload.system;
2575
2583
  }
2584
+ if (Array.isArray(payload.messages)) {
2585
+ payload.messages = payload.messages.filter((m) => {
2586
+ if (m?.role !== "system" && m?.role !== "developer") return true;
2587
+ const text = typeof m.content === "string" ? m.content : Array.isArray(m.content) ? m.content.map((b) => typeof b === "string" ? b : b?.text ?? "").filter(Boolean).join("\n\n") : "";
2588
+ if (text) sysParts.push(text);
2589
+ return false;
2590
+ });
2591
+ }
2592
+ if (sysParts.length) {
2593
+ if (!Array.isArray(payload.messages)) payload.messages = [];
2594
+ payload.messages.unshift({ role: "system", content: sysParts.join("\n\n") });
2595
+ }
2576
2596
  if (payload.tool_choice && typeof payload.tool_choice === "object") {
2577
2597
  const tc = payload.tool_choice;
2578
2598
  if (tc.type === "any") {
@@ -3583,6 +3603,17 @@ var server = createServer((req, res) => {
3583
3603
  );
3584
3604
  }
3585
3605
  });
3606
+ if (status < 200 || status >= 300) {
3607
+ let errBody = "";
3608
+ proxyRes.on("data", (c) => {
3609
+ if (errBody.length < 300) errBody += c.toString("utf8");
3610
+ });
3611
+ proxyRes.on("end", () => {
3612
+ console.error(
3613
+ `[Proxy] upstream error: ${resolvedTarget.provider}:${resolvedTarget.model} status=${status} body=${JSON.stringify(errBody.slice(0, 300))}`
3614
+ );
3615
+ });
3616
+ }
3586
3617
  const contentType = proxyRes.headers["content-type"] || "";
3587
3618
  const isStreaming = payload.stream === true && /text\/event-stream/i.test(contentType);
3588
3619
  const retryEmptyTurn = () => {