mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-30 11:52:13 +03:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5d7772ecb0 | ||
|
|
56ce618eca | ||
|
|
b0381c7542 | ||
|
|
b328ed5fa9 | ||
|
|
7d72f1711f | ||
|
|
cd05e03d63 | ||
|
|
e25029939d | ||
|
|
53de27417d |
22
CHANGELOG.md
22
CHANGELOG.md
@@ -4,6 +4,28 @@
|
||||
|
||||
---
|
||||
|
||||
## [2.6.2] — 2026-03-16
|
||||
|
||||
> Sprint: module hashing fully fixed, 2 PRs merged (Anthropic tools filter + custom endpoint paths), Alibaba Cloud DashScope provider added, 3 stale issues closed.
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- **fix(build)**: Extended webpack `externals` hash-strip to cover ALL `serverExternalPackages`, not just `better-sqlite3`. Next.js 16 Turbopack hashes `zod`, `pino`, and every other server-external package into names like `zod-dcb22c6336e0bc69` that don't exist in `node_modules` at runtime. A HASH_PATTERN regex catch-all now strips the 16-char suffix and falls back to the base package name. Also added `NEXT_PRIVATE_BUILD_WORKER=0` in `prepublish.mjs` to reinforce webpack mode, plus a post-build scan that reports any remaining hashed refs. (#396, #398, PR #403)
|
||||
- **fix(chat)**: Anthropic-format tool names (`tool.name` without `.function` wrapper) were silently dropped by the empty-name filter introduced in #346. LiteLLM proxies requests with `anthropic/` prefix in Anthropic Messages API format, causing all tools to be filtered and Anthropic to return `400: tool_choice.any may only be specified while providing tools`. Fixed by falling back to `tool.name` when `tool.function.name` is absent. Added 8 regression unit tests. (PR #397)
|
||||
|
||||
### ✨ Features
|
||||
|
||||
- **feat(api)**: Custom endpoint paths for OpenAI-compatible provider nodes — configure `chatPath` and `modelsPath` per node (e.g. `/v4/chat/completions`) in the provider connection UI. Includes a DB migration (`003_provider_node_custom_paths.sql`) and URL path sanitization (no `..` traversal, must start with `/`). (PR #400)
|
||||
- **feat(provider)**: Alibaba Cloud DashScope added as OpenAI-compatible provider. International endpoint: `dashscope-intl.aliyuncs.com/compatible-mode/v1`. 12 models: `qwen-max`, `qwen-plus`, `qwen-turbo`, `qwen3-coder-plus/flash`, `qwq-plus`, `qwq-32b`, `qwen3-32b`, `qwen3-235b-a22b`. Auth: Bearer API key.
|
||||
|
||||
### 📋 Issues Closed
|
||||
|
||||
- **close #323**: Cline connection error `[object Object]` — fixed in v2.3.7; instructed user to upgrade from v2.2.9
|
||||
- **close #337**: Kiro credit tracking — implemented in v2.5.5 (#381); pointed user to Dashboard → Usage
|
||||
- **triage #402**: ARM64 macOS DMG damaged — requested macOS version, exact error, and advised `xattr -d com.apple.quarantine` workaround
|
||||
|
||||
---
|
||||
|
||||
## [2.6.1] — 2026-03-15
|
||||
|
||||
> Critical startup fix: v2.6.0 global npm installs crashed with a 500 error due to a Turbopack/webpack module-name hashing bug in the Next.js 16 instrumentation hook.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
openapi: 3.1.0
|
||||
info:
|
||||
title: OmniRoute API
|
||||
version: 2.6.1
|
||||
version: 2.6.2
|
||||
description: |
|
||||
OmniRoute is a local-first AI API proxy router. It provides an OpenAI-compatible
|
||||
endpoint that routes requests to multiple AI providers with load balancing,
|
||||
|
||||
@@ -39,25 +39,60 @@ const nextConfig = {
|
||||
},
|
||||
webpack: (config, { isServer }) => {
|
||||
if (isServer) {
|
||||
// Force better-sqlite3 to always be required by its exact package name.
|
||||
// ── Turbopack / Next.js 16 module-hash patch (#394, #396, #398) ────────
|
||||
//
|
||||
// Next.js 16 webpack compiles the instrumentation hook into a separate
|
||||
// chunk ([root-of-the-server]__<hash>.js) and can emit a hashed require
|
||||
// such as `require('better-sqlite3-90e2652d1716b047')` even when the
|
||||
// package is listed in `serverExternalPackages`. That hashed name doesn't
|
||||
// exist in node_modules, causing the 500 error reported in issue #394.
|
||||
// Next.js 16 (with or without Turbopack) compiles the instrumentation hook
|
||||
// into a separate chunk and emits hashed require() calls such as:
|
||||
// require('better-sqlite3-90e2652d1716b047')
|
||||
// require('zod-dcb22c6336e0bc69')
|
||||
// require('pino-28069d5257187539')
|
||||
//
|
||||
// Adding an explicit `externals` function overrides the bundler's default
|
||||
// handling and always emits `require('better-sqlite3')`, which resolves
|
||||
// correctly in the published standalone build.
|
||||
// These hashed names don't exist in node_modules and cause a 500 at
|
||||
// startup on all npm global installs (issues #394, #396, #398).
|
||||
//
|
||||
// We use two strategies:
|
||||
// 1. Exact-name externals for all known server-side packages.
|
||||
// 2. Hash-strip catch-all: any require('<name>-<16hexchars>' strips the
|
||||
// suffix and falls through to the real package name.
|
||||
//
|
||||
const HASH_PATTERN = /^(.+)-[0-9a-f]{16}$/;
|
||||
|
||||
const KNOWN_EXTERNALS = new Set([
|
||||
"better-sqlite3",
|
||||
"zod",
|
||||
"pino",
|
||||
"pino-pretty",
|
||||
"child_process",
|
||||
"fs",
|
||||
"path",
|
||||
"os",
|
||||
"crypto",
|
||||
"net",
|
||||
"tls",
|
||||
"http",
|
||||
"https",
|
||||
"stream",
|
||||
"buffer",
|
||||
"util",
|
||||
]);
|
||||
|
||||
const prev = config.externals ?? [];
|
||||
const prevArr = Array.isArray(prev) ? prev : [prev];
|
||||
config.externals = [
|
||||
...prevArr,
|
||||
({ request }, callback) => {
|
||||
if (request === "better-sqlite3") {
|
||||
// Case 1: Exact known package — treat as external
|
||||
if (KNOWN_EXTERNALS.has(request)) {
|
||||
return callback(null, `commonjs ${request}`);
|
||||
}
|
||||
// Case 2: Hash-suffixed name — strip hash, use base name
|
||||
// e.g. "better-sqlite3-90e2652d1716b047" → "better-sqlite3"
|
||||
// "zod-dcb22c6336e0bc69" → "zod"
|
||||
const hashMatch = request?.match?.(HASH_PATTERN);
|
||||
if (hashMatch) {
|
||||
const baseName = hashMatch[1];
|
||||
return callback(null, `commonjs ${baseName}`);
|
||||
}
|
||||
callback();
|
||||
},
|
||||
];
|
||||
@@ -75,6 +110,7 @@ const nextConfig = {
|
||||
}
|
||||
return config;
|
||||
},
|
||||
|
||||
async rewrites() {
|
||||
return [
|
||||
{
|
||||
|
||||
@@ -99,11 +99,11 @@ export class BaseExecutor {
|
||||
void model;
|
||||
void stream;
|
||||
if (this.provider?.startsWith?.("openai-compatible-")) {
|
||||
const baseUrl =
|
||||
typeof credentials?.providerSpecificData?.baseUrl === "string"
|
||||
? credentials.providerSpecificData.baseUrl
|
||||
: "https://api.openai.com/v1";
|
||||
const psd = credentials?.providerSpecificData;
|
||||
const baseUrl = typeof psd?.baseUrl === "string" ? psd.baseUrl : "https://api.openai.com/v1";
|
||||
const normalized = baseUrl.replace(/\/$/, "");
|
||||
const customPath = typeof psd?.chatPath === "string" && psd.chatPath ? psd.chatPath : null;
|
||||
if (customPath) return `${normalized}${customPath}`;
|
||||
const path = this.provider.includes("responses") ? "/responses" : "/chat/completions";
|
||||
return `${normalized}${path}`;
|
||||
}
|
||||
|
||||
@@ -9,15 +9,20 @@ export class DefaultExecutor extends BaseExecutor {
|
||||
|
||||
buildUrl(model, stream, urlIndex = 0, credentials = null) {
|
||||
if (this.provider?.startsWith?.("openai-compatible-")) {
|
||||
const baseUrl = credentials?.providerSpecificData?.baseUrl || "https://api.openai.com/v1";
|
||||
const psd = credentials?.providerSpecificData;
|
||||
const baseUrl = psd?.baseUrl || "https://api.openai.com/v1";
|
||||
const normalized = baseUrl.replace(/\/$/, "");
|
||||
const customPath = typeof psd?.chatPath === "string" && psd.chatPath ? psd.chatPath : null;
|
||||
if (customPath) return `${normalized}${customPath}`;
|
||||
const path = this.provider.includes("responses") ? "/responses" : "/chat/completions";
|
||||
return `${normalized}${path}`;
|
||||
}
|
||||
if (this.provider?.startsWith?.("anthropic-compatible-")) {
|
||||
const baseUrl = credentials?.providerSpecificData?.baseUrl || "https://api.anthropic.com/v1";
|
||||
const psd = credentials?.providerSpecificData;
|
||||
const baseUrl = psd?.baseUrl || "https://api.anthropic.com/v1";
|
||||
const normalized = baseUrl.replace(/\/$/, "");
|
||||
return `${normalized}/messages`;
|
||||
const customPath = typeof psd?.chatPath === "string" && psd.chatPath ? psd.chatPath : null;
|
||||
return `${normalized}${customPath || "/messages"}`;
|
||||
}
|
||||
switch (this.provider) {
|
||||
case "claude":
|
||||
|
||||
@@ -217,14 +217,16 @@ export async function handleChatCore({
|
||||
return item;
|
||||
});
|
||||
}
|
||||
// ── #346: Strip tools with empty function.name ──
|
||||
// ── #346: Strip tools with empty name ──
|
||||
// Claude Code sometimes forwards tool definitions with empty names, causing
|
||||
// OpenAI-compatible upstream providers to reject with:
|
||||
// "Invalid 'input[N].name': empty string. Expected minimum length 1."
|
||||
// Handles both OpenAI format ({ function: { name } }) and Anthropic format ({ name }).
|
||||
if (Array.isArray(translatedBody.tools)) {
|
||||
translatedBody.tools = translatedBody.tools.filter((tool: Record<string, unknown>) => {
|
||||
const fn = tool.function as Record<string, unknown> | undefined;
|
||||
return fn?.name && String(fn.name).trim().length > 0;
|
||||
const name = fn?.name ?? tool.name;
|
||||
return name && String(name).trim().length > 0;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "omniroute",
|
||||
"version": "2.6.0",
|
||||
"version": "2.6.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "omniroute",
|
||||
"version": "2.6.0",
|
||||
"version": "2.6.2",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"workspaces": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "omniroute",
|
||||
"version": "2.6.1",
|
||||
"version": "2.6.2",
|
||||
"description": "Smart AI Router with auto fallback — route to FREE & cheap models, zero downtime. Works with Cursor, Cline, Claude Desktop, Codex, and any OpenAI-compatible tool.",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
|
||||
@@ -10,7 +10,16 @@
|
||||
*/
|
||||
|
||||
import { execSync } from "node:child_process";
|
||||
import { existsSync, mkdirSync, cpSync, rmSync, writeFileSync, readFileSync } from "node:fs";
|
||||
import {
|
||||
existsSync,
|
||||
mkdirSync,
|
||||
cpSync,
|
||||
rmSync,
|
||||
writeFileSync,
|
||||
readFileSync,
|
||||
readdirSync,
|
||||
statSync,
|
||||
} from "node:fs";
|
||||
import { join, dirname } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
@@ -37,7 +46,13 @@ console.log(" 🏗️ Building Next.js (standalone)...");
|
||||
execSync("npx next build", {
|
||||
cwd: ROOT,
|
||||
stdio: "inherit",
|
||||
env: { ...process.env, EXPERIMENTAL_TURBOPACK: "0" },
|
||||
env: {
|
||||
...process.env,
|
||||
// Force webpack codegen — Turbopack emits hashed require() calls for
|
||||
// server external packages that break npm global installs (#394, #396, #398).
|
||||
EXPERIMENTAL_TURBOPACK: "0",
|
||||
NEXT_PRIVATE_BUILD_WORKER: "0",
|
||||
},
|
||||
});
|
||||
|
||||
// ── Step 4: Verify standalone output ───────────────────────
|
||||
@@ -51,6 +66,46 @@ if (!existsSync(serverJs)) {
|
||||
}
|
||||
|
||||
// ── Step 5: Copy standalone output to app/ ─────────────────
|
||||
// ── Step 4.5: Check build for hashed external references ──────────────────────
|
||||
// Warn if Turbopack-style hash suffixes are found — they will be resolved at
|
||||
// runtime by the externals patch in next.config.mjs, but log for visibility.
|
||||
{
|
||||
const HASH_RE = /require\(["']([\w@./-]+-[0-9a-f]{16})["']\)/;
|
||||
const scanDir = (dir, hits = []) => {
|
||||
let entries = [];
|
||||
try {
|
||||
entries = readdirSync(dir);
|
||||
} catch {
|
||||
return hits;
|
||||
}
|
||||
for (const e of entries) {
|
||||
const f = join(dir, e);
|
||||
try {
|
||||
if (statSync(f).isDirectory()) {
|
||||
scanDir(f, hits);
|
||||
continue;
|
||||
}
|
||||
if (!f.endsWith(".js")) continue;
|
||||
const m = readFileSync(f, "utf8").match(HASH_RE);
|
||||
if (m) hits.push({ file: f.replace(standaloneDir, "app"), mod: m[1] });
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
return hits;
|
||||
};
|
||||
const hits = scanDir(join(standaloneDir, ".next", "server"));
|
||||
if (hits.length > 0) {
|
||||
console.warn(
|
||||
" ⚠️ Hashed externals in build (will be auto-fixed at runtime by externals patch):"
|
||||
);
|
||||
hits.slice(0, 5).forEach((h) => console.warn());
|
||||
if (hits.length > 5) console.warn();
|
||||
} else {
|
||||
console.log(" ✅ Build clean — no hashed externals found.");
|
||||
}
|
||||
}
|
||||
|
||||
console.log(" 📋 Copying standalone build to app/...");
|
||||
mkdirSync(APP_DIR, { recursive: true });
|
||||
cpSync(standaloneDir, APP_DIR, { recursive: true });
|
||||
|
||||
@@ -3075,11 +3075,14 @@ function EditCompatibleNodeModal({ isOpen, node, onSave, onClose, isAnthropic })
|
||||
prefix: "",
|
||||
apiType: "chat",
|
||||
baseUrl: "https://api.openai.com/v1",
|
||||
chatPath: "",
|
||||
modelsPath: "",
|
||||
});
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [checkKey, setCheckKey] = useState("");
|
||||
const [validating, setValidating] = useState(false);
|
||||
const [validationResult, setValidationResult] = useState(null);
|
||||
const [showAdvanced, setShowAdvanced] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (node) {
|
||||
@@ -3090,7 +3093,10 @@ function EditCompatibleNodeModal({ isOpen, node, onSave, onClose, isAnthropic })
|
||||
baseUrl:
|
||||
node.baseUrl ||
|
||||
(isAnthropic ? "https://api.anthropic.com/v1" : "https://api.openai.com/v1"),
|
||||
chatPath: node.chatPath || "",
|
||||
modelsPath: node.modelsPath || "",
|
||||
});
|
||||
setShowAdvanced(!!(node.chatPath || node.modelsPath));
|
||||
}
|
||||
}, [node, isAnthropic]);
|
||||
|
||||
@@ -3107,6 +3113,8 @@ function EditCompatibleNodeModal({ isOpen, node, onSave, onClose, isAnthropic })
|
||||
name: formData.name,
|
||||
prefix: formData.prefix,
|
||||
baseUrl: formData.baseUrl,
|
||||
chatPath: formData.chatPath || "",
|
||||
modelsPath: formData.modelsPath || "",
|
||||
};
|
||||
if (!isAnthropic) {
|
||||
payload.apiType = formData.apiType;
|
||||
@@ -3127,6 +3135,7 @@ function EditCompatibleNodeModal({ isOpen, node, onSave, onClose, isAnthropic })
|
||||
baseUrl: formData.baseUrl,
|
||||
apiKey: checkKey,
|
||||
type: isAnthropic ? "anthropic-compatible" : "openai-compatible",
|
||||
modelsPath: formData.modelsPath || "",
|
||||
}),
|
||||
});
|
||||
const data = await res.json();
|
||||
@@ -3182,6 +3191,39 @@ function EditCompatibleNodeModal({ isOpen, node, onSave, onClose, isAnthropic })
|
||||
type: isAnthropic ? t("anthropic") : t("openai"),
|
||||
})}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="text-sm text-text-muted hover:text-text-primary flex items-center gap-1"
|
||||
onClick={() => setShowAdvanced(!showAdvanced)}
|
||||
aria-expanded={showAdvanced}
|
||||
aria-controls="advanced-settings"
|
||||
>
|
||||
<span
|
||||
className={`transition-transform ${showAdvanced ? "rotate-90" : ""}`}
|
||||
aria-hidden="true"
|
||||
>
|
||||
▶
|
||||
</span>
|
||||
{t("advancedSettings")}
|
||||
</button>
|
||||
{showAdvanced && (
|
||||
<div id="advanced-settings" className="flex flex-col gap-3 pl-2 border-l-2 border-border">
|
||||
<Input
|
||||
label={t("chatPathLabel")}
|
||||
value={formData.chatPath}
|
||||
onChange={(e) => setFormData({ ...formData, chatPath: e.target.value })}
|
||||
placeholder={isAnthropic ? "/messages" : t("chatPathPlaceholder")}
|
||||
hint={t("chatPathHint")}
|
||||
/>
|
||||
<Input
|
||||
label={t("modelsPathLabel")}
|
||||
value={formData.modelsPath}
|
||||
onChange={(e) => setFormData({ ...formData, modelsPath: e.target.value })}
|
||||
placeholder={t("modelsPathPlaceholder")}
|
||||
hint={t("modelsPathHint")}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
label={t("apiKeyForCheck")}
|
||||
@@ -3232,6 +3274,8 @@ EditCompatibleNodeModal.propTypes = {
|
||||
prefix: PropTypes.string,
|
||||
apiType: PropTypes.string,
|
||||
baseUrl: PropTypes.string,
|
||||
chatPath: PropTypes.string,
|
||||
modelsPath: PropTypes.string,
|
||||
}),
|
||||
onSave: PropTypes.func.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
|
||||
@@ -772,11 +772,14 @@ function AddOpenAICompatibleModal({ isOpen, onClose, onCreated }) {
|
||||
prefix: "",
|
||||
apiType: "chat",
|
||||
baseUrl: "https://api.openai.com/v1",
|
||||
chatPath: "",
|
||||
modelsPath: "",
|
||||
});
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [checkKey, setCheckKey] = useState("");
|
||||
const [validating, setValidating] = useState(false);
|
||||
const [validationResult, setValidationResult] = useState<"success" | "failed" | null>(null);
|
||||
const [showAdvanced, setShowAdvanced] = useState(false);
|
||||
|
||||
const apiTypeOptions = [
|
||||
{ value: "chat", label: t("chatCompletions") },
|
||||
@@ -804,6 +807,8 @@ function AddOpenAICompatibleModal({ isOpen, onClose, onCreated }) {
|
||||
apiType: formData.apiType,
|
||||
baseUrl: formData.baseUrl,
|
||||
type: "openai-compatible",
|
||||
chatPath: formData.chatPath || "",
|
||||
modelsPath: formData.modelsPath || "",
|
||||
}),
|
||||
});
|
||||
const data = await res.json();
|
||||
@@ -814,9 +819,12 @@ function AddOpenAICompatibleModal({ isOpen, onClose, onCreated }) {
|
||||
prefix: "",
|
||||
apiType: "chat",
|
||||
baseUrl: "https://api.openai.com/v1",
|
||||
chatPath: "",
|
||||
modelsPath: "",
|
||||
});
|
||||
setCheckKey("");
|
||||
setValidationResult(null);
|
||||
setShowAdvanced(false);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("Error creating OpenAI Compatible node:", error);
|
||||
@@ -835,6 +843,7 @@ function AddOpenAICompatibleModal({ isOpen, onClose, onCreated }) {
|
||||
baseUrl: formData.baseUrl,
|
||||
apiKey: checkKey,
|
||||
type: "openai-compatible",
|
||||
modelsPath: formData.modelsPath || "",
|
||||
}),
|
||||
});
|
||||
const data = await res.json();
|
||||
@@ -876,6 +885,39 @@ function AddOpenAICompatibleModal({ isOpen, onClose, onCreated }) {
|
||||
placeholder={t("openaiBaseUrlPlaceholder")}
|
||||
hint={t("compatibleBaseUrlHint", { type: t("openai") })}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="text-sm text-text-muted hover:text-text-primary flex items-center gap-1"
|
||||
onClick={() => setShowAdvanced(!showAdvanced)}
|
||||
aria-expanded={showAdvanced}
|
||||
aria-controls="advanced-settings"
|
||||
>
|
||||
<span
|
||||
className={`transition-transform ${showAdvanced ? "rotate-90" : ""}`}
|
||||
aria-hidden="true"
|
||||
>
|
||||
▶
|
||||
</span>
|
||||
{t("advancedSettings")}
|
||||
</button>
|
||||
{showAdvanced && (
|
||||
<div id="advanced-settings" className="flex flex-col gap-3 pl-2 border-l-2 border-border">
|
||||
<Input
|
||||
label={t("chatPathLabel")}
|
||||
value={formData.chatPath}
|
||||
onChange={(e) => setFormData({ ...formData, chatPath: e.target.value })}
|
||||
placeholder={t("chatPathPlaceholder")}
|
||||
hint={t("chatPathHint")}
|
||||
/>
|
||||
<Input
|
||||
label={t("modelsPathLabel")}
|
||||
value={formData.modelsPath}
|
||||
onChange={(e) => setFormData({ ...formData, modelsPath: e.target.value })}
|
||||
placeholder={t("modelsPathPlaceholder")}
|
||||
hint={t("modelsPathHint")}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
label={t("apiKeyForCheck")}
|
||||
@@ -933,11 +975,14 @@ function AddAnthropicCompatibleModal({ isOpen, onClose, onCreated }) {
|
||||
name: "",
|
||||
prefix: "",
|
||||
baseUrl: "https://api.anthropic.com/v1",
|
||||
chatPath: "",
|
||||
modelsPath: "",
|
||||
});
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [checkKey, setCheckKey] = useState("");
|
||||
const [validating, setValidating] = useState(false);
|
||||
const [validationResult, setValidationResult] = useState<"success" | "failed" | null>(null);
|
||||
const [showAdvanced, setShowAdvanced] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// Reset validation when modal opens
|
||||
@@ -959,6 +1004,8 @@ function AddAnthropicCompatibleModal({ isOpen, onClose, onCreated }) {
|
||||
prefix: formData.prefix,
|
||||
baseUrl: formData.baseUrl,
|
||||
type: "anthropic-compatible",
|
||||
chatPath: formData.chatPath || "",
|
||||
modelsPath: formData.modelsPath || "",
|
||||
}),
|
||||
});
|
||||
const data = await res.json();
|
||||
@@ -968,9 +1015,12 @@ function AddAnthropicCompatibleModal({ isOpen, onClose, onCreated }) {
|
||||
name: "",
|
||||
prefix: "",
|
||||
baseUrl: "https://api.anthropic.com/v1",
|
||||
chatPath: "",
|
||||
modelsPath: "",
|
||||
});
|
||||
setCheckKey("");
|
||||
setValidationResult(null);
|
||||
setShowAdvanced(false);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("Error creating Anthropic Compatible node:", error);
|
||||
@@ -989,6 +1039,7 @@ function AddAnthropicCompatibleModal({ isOpen, onClose, onCreated }) {
|
||||
baseUrl: formData.baseUrl,
|
||||
apiKey: checkKey,
|
||||
type: "anthropic-compatible",
|
||||
modelsPath: formData.modelsPath || "",
|
||||
}),
|
||||
});
|
||||
const data = await res.json();
|
||||
@@ -1024,6 +1075,39 @@ function AddAnthropicCompatibleModal({ isOpen, onClose, onCreated }) {
|
||||
placeholder={t("anthropicBaseUrlPlaceholder")}
|
||||
hint={t("compatibleBaseUrlHint", { type: t("anthropic") })}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="text-sm text-text-muted hover:text-text-primary flex items-center gap-1"
|
||||
onClick={() => setShowAdvanced(!showAdvanced)}
|
||||
aria-expanded={showAdvanced}
|
||||
aria-controls="advanced-settings"
|
||||
>
|
||||
<span
|
||||
className={`transition-transform ${showAdvanced ? "rotate-90" : ""}`}
|
||||
aria-hidden="true"
|
||||
>
|
||||
▶
|
||||
</span>
|
||||
{t("advancedSettings")}
|
||||
</button>
|
||||
{showAdvanced && (
|
||||
<div id="advanced-settings" className="flex flex-col gap-3 pl-2 border-l-2 border-border">
|
||||
<Input
|
||||
label={t("chatPathLabel")}
|
||||
value={formData.chatPath}
|
||||
onChange={(e) => setFormData({ ...formData, chatPath: e.target.value })}
|
||||
placeholder="/messages"
|
||||
hint={t("chatPathHint")}
|
||||
/>
|
||||
<Input
|
||||
label={t("modelsPathLabel")}
|
||||
value={formData.modelsPath}
|
||||
onChange={(e) => setFormData({ ...formData, modelsPath: e.target.value })}
|
||||
placeholder={t("modelsPathPlaceholder")}
|
||||
hint={t("modelsPathHint")}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
label={t("apiKeyForCheck")}
|
||||
|
||||
@@ -39,7 +39,7 @@ export async function PUT(request: Request, { params }: { params: Promise<{ id:
|
||||
if (isValidationFailure(validation)) {
|
||||
return NextResponse.json({ error: validation.error }, { status: 400 });
|
||||
}
|
||||
const { name, prefix, apiType, baseUrl } = validation.data;
|
||||
const { name, prefix, apiType, baseUrl, chatPath, modelsPath } = validation.data;
|
||||
const node: any = await getProviderNodeById(id);
|
||||
|
||||
if (!node) {
|
||||
@@ -68,6 +68,8 @@ export async function PUT(request: Request, { params }: { params: Promise<{ id:
|
||||
name: name.trim(),
|
||||
prefix: prefix.trim(),
|
||||
baseUrl: sanitizedBaseUrl,
|
||||
chatPath: chatPath || null,
|
||||
modelsPath: modelsPath || null,
|
||||
};
|
||||
|
||||
if (node.type === "openai-compatible") {
|
||||
@@ -88,6 +90,8 @@ export async function PUT(request: Request, { params }: { params: Promise<{ id:
|
||||
prefix: prefix.trim(),
|
||||
baseUrl: sanitizedBaseUrl,
|
||||
nodeName: updated.name,
|
||||
chatPath: updated.chatPath || undefined,
|
||||
modelsPath: updated.modelsPath || undefined,
|
||||
} as JsonRecord;
|
||||
if (node.type === "openai-compatible") {
|
||||
providerSpecificData.apiType = apiType;
|
||||
|
||||
@@ -49,7 +49,7 @@ export async function POST(request) {
|
||||
if (isValidationFailure(validation)) {
|
||||
return NextResponse.json({ error: validation.error }, { status: 400 });
|
||||
}
|
||||
const { name, prefix, apiType, baseUrl, type } = validation.data;
|
||||
const { name, prefix, apiType, baseUrl, type, chatPath, modelsPath } = validation.data;
|
||||
|
||||
// Determine type
|
||||
const nodeType = type || "openai-compatible";
|
||||
@@ -62,6 +62,8 @@ export async function POST(request) {
|
||||
apiType,
|
||||
baseUrl: (baseUrl || OPENAI_COMPATIBLE_DEFAULTS.baseUrl).trim(),
|
||||
name: name.trim(),
|
||||
chatPath: chatPath || null,
|
||||
modelsPath: modelsPath || null,
|
||||
});
|
||||
return NextResponse.json({ node }, { status: 201 });
|
||||
}
|
||||
@@ -82,6 +84,8 @@ export async function POST(request) {
|
||||
prefix: prefix.trim(),
|
||||
baseUrl: sanitizedBaseUrl,
|
||||
name: name.trim(),
|
||||
chatPath: chatPath || null,
|
||||
modelsPath: modelsPath || null,
|
||||
});
|
||||
return NextResponse.json({ node }, { status: 201 });
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ export async function POST(request) {
|
||||
if (isValidationFailure(validation)) {
|
||||
return NextResponse.json({ error: validation.error }, { status: 400 });
|
||||
}
|
||||
const { baseUrl, apiKey, type } = validation.data;
|
||||
const { baseUrl, apiKey, type, modelsPath } = validation.data;
|
||||
|
||||
// Anthropic Compatible Validation
|
||||
if (type === "anthropic-compatible") {
|
||||
@@ -35,7 +35,7 @@ export async function POST(request) {
|
||||
}
|
||||
|
||||
// Use /models endpoint for validation as many compatible providers support it (like OpenAI)
|
||||
const modelsUrl = `${normalizedBase}/models`;
|
||||
const modelsUrl = `${normalizedBase}${modelsPath || "/models"}`;
|
||||
|
||||
const res = await fetch(modelsUrl, {
|
||||
method: "GET",
|
||||
@@ -50,7 +50,7 @@ export async function POST(request) {
|
||||
}
|
||||
|
||||
// OpenAI Compatible Validation (Default)
|
||||
const modelsUrl = `${baseUrl.replace(/\/$/, "")}/models`;
|
||||
const modelsUrl = `${baseUrl.replace(/\/$/, "")}${modelsPath || "/models"}`;
|
||||
const res = await fetch(modelsUrl, {
|
||||
headers: { Authorization: `Bearer ${apiKey}` },
|
||||
});
|
||||
|
||||
@@ -82,6 +82,8 @@ export async function POST(request: Request) {
|
||||
apiType: node.apiType,
|
||||
baseUrl: node.baseUrl,
|
||||
nodeName: node.name,
|
||||
...(node.chatPath ? { chatPath: node.chatPath } : {}),
|
||||
...(node.modelsPath ? { modelsPath: node.modelsPath } : {}),
|
||||
};
|
||||
} else if (isAnthropicCompatibleProvider(provider)) {
|
||||
const node: any = await getProviderNodeById(provider);
|
||||
@@ -101,6 +103,8 @@ export async function POST(request: Request) {
|
||||
prefix: node.prefix,
|
||||
baseUrl: node.baseUrl,
|
||||
nodeName: node.name,
|
||||
...(node.chatPath ? { chatPath: node.chatPath } : {}),
|
||||
...(node.modelsPath ? { modelsPath: node.modelsPath } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1386,10 +1386,17 @@
|
||||
"failed": "فشل",
|
||||
"leaveBlankKeepCurrentApiKey": "اتركه فارغًا للاحتفاظ بمفتاح API الحالي.",
|
||||
"editCompatibleTitle": "تحرير {type} متوافق",
|
||||
"compatibleBaseUrlHint": "استخدم عنوان URL الأساسي (الذي ينتهي بـ /v1) لواجهة برمجة التطبيقات المتوافقة مع {type}.",
|
||||
"compatibleBaseUrlHint": "Root URL of your {type}-compatible API. Use Advanced Settings for custom endpoint paths.",
|
||||
"apiKeyForCheck": "مفتاح API (للفحص)",
|
||||
"compatibleProdPlaceholder": "{type} متوافق (المنتج)",
|
||||
"providerTestTimeout": "Provider test timed out — too many connections to test at once"
|
||||
"providerTestTimeout": "Provider test timed out — too many connections to test at once",
|
||||
"advancedSettings": "Advanced Settings",
|
||||
"chatPathLabel": "Chat Endpoint Path",
|
||||
"chatPathPlaceholder": "/chat/completions",
|
||||
"chatPathHint": "Custom chat path for providers with non-standard APIs (e.g. /v4/chat/completions)",
|
||||
"modelsPathLabel": "Models Endpoint Path",
|
||||
"modelsPathPlaceholder": "/models",
|
||||
"modelsPathHint": "Custom models path for validation (e.g. /v4/models)"
|
||||
},
|
||||
"settings": {
|
||||
"title": "الإعدادات",
|
||||
|
||||
@@ -1386,10 +1386,17 @@
|
||||
"failed": "Неуспешно",
|
||||
"leaveBlankKeepCurrentApiKey": "Оставете празно, за да запазите текущия API ключ.",
|
||||
"editCompatibleTitle": "Редактиране {type} Съвместим",
|
||||
"compatibleBaseUrlHint": "Използвайте основния URL (завършващ на /v1) за вашия {type}-съвместим API.",
|
||||
"compatibleBaseUrlHint": "Root URL of your {type}-compatible API. Use Advanced Settings for custom endpoint paths.",
|
||||
"apiKeyForCheck": "API ключ (за проверка)",
|
||||
"compatibleProdPlaceholder": "{type} Съвместим (Prod)",
|
||||
"providerTestTimeout": "Provider test timed out — too many connections to test at once"
|
||||
"providerTestTimeout": "Provider test timed out — too many connections to test at once",
|
||||
"advancedSettings": "Advanced Settings",
|
||||
"chatPathLabel": "Chat Endpoint Path",
|
||||
"chatPathPlaceholder": "/chat/completions",
|
||||
"chatPathHint": "Custom chat path for providers with non-standard APIs (e.g. /v4/chat/completions)",
|
||||
"modelsPathLabel": "Models Endpoint Path",
|
||||
"modelsPathPlaceholder": "/models",
|
||||
"modelsPathHint": "Custom models path for validation (e.g. /v4/models)"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Настройки",
|
||||
|
||||
@@ -1386,10 +1386,17 @@
|
||||
"failed": "Mislykkedes",
|
||||
"leaveBlankKeepCurrentApiKey": "Lad stå tomt for at beholde den aktuelle API-nøgle.",
|
||||
"editCompatibleTitle": "Rediger {type} Kompatibel",
|
||||
"compatibleBaseUrlHint": "Brug basis-URL'en (der slutter på /v1) til din {type}-kompatible API.",
|
||||
"compatibleBaseUrlHint": "Root URL of your {type}-compatible API. Use Advanced Settings for custom endpoint paths.",
|
||||
"apiKeyForCheck": "API-nøgle (til check)",
|
||||
"compatibleProdPlaceholder": "{type} Kompatibel (Prod)",
|
||||
"providerTestTimeout": "Provider test timed out — too many connections to test at once"
|
||||
"providerTestTimeout": "Provider test timed out — too many connections to test at once",
|
||||
"advancedSettings": "Advanced Settings",
|
||||
"chatPathLabel": "Chat Endpoint Path",
|
||||
"chatPathPlaceholder": "/chat/completions",
|
||||
"chatPathHint": "Custom chat path for providers with non-standard APIs (e.g. /v4/chat/completions)",
|
||||
"modelsPathLabel": "Models Endpoint Path",
|
||||
"modelsPathPlaceholder": "/models",
|
||||
"modelsPathHint": "Custom models path for validation (e.g. /v4/models)"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Indstillinger",
|
||||
|
||||
@@ -1386,10 +1386,17 @@
|
||||
"failed": "Fehlgeschlagen",
|
||||
"leaveBlankKeepCurrentApiKey": "Lassen Sie das Feld leer, um den aktuellen API-Schlüssel beizubehalten.",
|
||||
"editCompatibleTitle": "Bearbeiten Sie {type} kompatibel",
|
||||
"compatibleBaseUrlHint": "Verwenden Sie die Basis-URL (die auf /v1 endet) für Ihre {type}-kompatible API.",
|
||||
"compatibleBaseUrlHint": "Root URL of your {type}-compatible API. Use Advanced Settings for custom endpoint paths.",
|
||||
"apiKeyForCheck": "API-Schlüssel (zur Überprüfung)",
|
||||
"compatibleProdPlaceholder": "{type} Kompatibel (Prod)",
|
||||
"providerTestTimeout": "Provider test timed out — too many connections to test at once"
|
||||
"providerTestTimeout": "Provider test timed out — too many connections to test at once",
|
||||
"advancedSettings": "Advanced Settings",
|
||||
"chatPathLabel": "Chat Endpoint Path",
|
||||
"chatPathPlaceholder": "/chat/completions",
|
||||
"chatPathHint": "Custom chat path for providers with non-standard APIs (e.g. /v4/chat/completions)",
|
||||
"modelsPathLabel": "Models Endpoint Path",
|
||||
"modelsPathPlaceholder": "/models",
|
||||
"modelsPathHint": "Custom models path for validation (e.g. /v4/models)"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Einstellungen",
|
||||
|
||||
@@ -1420,11 +1420,18 @@
|
||||
"failed": "Failed",
|
||||
"leaveBlankKeepCurrentApiKey": "Leave blank to keep the current API key.",
|
||||
"editCompatibleTitle": "Edit {type} Compatible",
|
||||
"compatibleBaseUrlHint": "Use the base URL (ending in /v1) for your {type}-compatible API.",
|
||||
"compatibleBaseUrlHint": "Root URL of your {type}-compatible API. Use Advanced Settings for custom endpoint paths.",
|
||||
"apiKeyForCheck": "API Key (for Check)",
|
||||
"compatibleProdPlaceholder": "{type} Compatible (Prod)",
|
||||
"tokenRefreshed": "Token refreshed successfully",
|
||||
"tokenRefreshFailed": "Token refresh failed"
|
||||
"tokenRefreshFailed": "Token refresh failed",
|
||||
"advancedSettings": "Advanced Settings",
|
||||
"chatPathLabel": "Chat Endpoint Path",
|
||||
"chatPathPlaceholder": "/chat/completions",
|
||||
"chatPathHint": "Custom chat path for providers with non-standard APIs (e.g. /v4/chat/completions)",
|
||||
"modelsPathLabel": "Models Endpoint Path",
|
||||
"modelsPathPlaceholder": "/models",
|
||||
"modelsPathHint": "Custom models path for validation (e.g. /v4/models)"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Settings",
|
||||
|
||||
@@ -1386,10 +1386,17 @@
|
||||
"failed": "Fallido",
|
||||
"leaveBlankKeepCurrentApiKey": "Déjelo en blanco para conservar la clave API actual.",
|
||||
"editCompatibleTitle": "Editar {type} Compatible",
|
||||
"compatibleBaseUrlHint": "Utilice la URL base (que termina en /v1) para su API compatible con {type}.",
|
||||
"compatibleBaseUrlHint": "Root URL of your {type}-compatible API. Use Advanced Settings for custom endpoint paths.",
|
||||
"apiKeyForCheck": "Clave API (para verificación)",
|
||||
"compatibleProdPlaceholder": "{type} Compatible (Prod.)",
|
||||
"providerTestTimeout": "Provider test timed out — too many connections to test at once"
|
||||
"providerTestTimeout": "Provider test timed out — too many connections to test at once",
|
||||
"advancedSettings": "Advanced Settings",
|
||||
"chatPathLabel": "Chat Endpoint Path",
|
||||
"chatPathPlaceholder": "/chat/completions",
|
||||
"chatPathHint": "Custom chat path for providers with non-standard APIs (e.g. /v4/chat/completions)",
|
||||
"modelsPathLabel": "Models Endpoint Path",
|
||||
"modelsPathPlaceholder": "/models",
|
||||
"modelsPathHint": "Custom models path for validation (e.g. /v4/models)"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Configuración",
|
||||
|
||||
@@ -1386,10 +1386,17 @@
|
||||
"failed": "Epäonnistui",
|
||||
"leaveBlankKeepCurrentApiKey": "Jätä tyhjäksi, jos haluat säilyttää nykyisen API-avaimen.",
|
||||
"editCompatibleTitle": "Muokkaa {type} Yhteensopiva",
|
||||
"compatibleBaseUrlHint": "Käytä perus-URL-osoitetta (päättyy /v1) {type}-yhteensopivalle API:lle.",
|
||||
"compatibleBaseUrlHint": "Root URL of your {type}-compatible API. Use Advanced Settings for custom endpoint paths.",
|
||||
"apiKeyForCheck": "API-avain (tarkistusta varten)",
|
||||
"compatibleProdPlaceholder": "{type} Yhteensopiva (tuote)",
|
||||
"providerTestTimeout": "Provider test timed out — too many connections to test at once"
|
||||
"providerTestTimeout": "Provider test timed out — too many connections to test at once",
|
||||
"advancedSettings": "Advanced Settings",
|
||||
"chatPathLabel": "Chat Endpoint Path",
|
||||
"chatPathPlaceholder": "/chat/completions",
|
||||
"chatPathHint": "Custom chat path for providers with non-standard APIs (e.g. /v4/chat/completions)",
|
||||
"modelsPathLabel": "Models Endpoint Path",
|
||||
"modelsPathPlaceholder": "/models",
|
||||
"modelsPathHint": "Custom models path for validation (e.g. /v4/models)"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Asetukset",
|
||||
|
||||
@@ -1386,10 +1386,17 @@
|
||||
"failed": "Échec",
|
||||
"leaveBlankKeepCurrentApiKey": "Laissez vide pour conserver la clé API actuelle.",
|
||||
"editCompatibleTitle": "Modifier {type} Compatible",
|
||||
"compatibleBaseUrlHint": "Utilisez l'URL de base (se terminant par /v1) pour votre API compatible {type}.",
|
||||
"compatibleBaseUrlHint": "Root URL of your {type}-compatible API. Use Advanced Settings for custom endpoint paths.",
|
||||
"apiKeyForCheck": "Clé API (pour vérification)",
|
||||
"compatibleProdPlaceholder": "{type} Compatible (Prod)",
|
||||
"providerTestTimeout": "Provider test timed out — too many connections to test at once"
|
||||
"providerTestTimeout": "Provider test timed out — too many connections to test at once",
|
||||
"advancedSettings": "Advanced Settings",
|
||||
"chatPathLabel": "Chat Endpoint Path",
|
||||
"chatPathPlaceholder": "/chat/completions",
|
||||
"chatPathHint": "Custom chat path for providers with non-standard APIs (e.g. /v4/chat/completions)",
|
||||
"modelsPathLabel": "Models Endpoint Path",
|
||||
"modelsPathPlaceholder": "/models",
|
||||
"modelsPathHint": "Custom models path for validation (e.g. /v4/models)"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Paramètres",
|
||||
|
||||
@@ -1386,10 +1386,17 @@
|
||||
"failed": "נכשל",
|
||||
"leaveBlankKeepCurrentApiKey": "השאר ריק כדי לשמור את מפתח ה-API הנוכחי.",
|
||||
"editCompatibleTitle": "ערוך {type} תואם",
|
||||
"compatibleBaseUrlHint": "השתמש בכתובת ה-URL הבסיסית (המסתיימת ב-/v1) עבור ה-API התואם {type} שלך.",
|
||||
"compatibleBaseUrlHint": "Root URL of your {type}-compatible API. Use Advanced Settings for custom endpoint paths.",
|
||||
"apiKeyForCheck": "מפתח API (לבדיקה)",
|
||||
"compatibleProdPlaceholder": "{type} תואם (פרוד)",
|
||||
"providerTestTimeout": "Provider test timed out — too many connections to test at once"
|
||||
"providerTestTimeout": "Provider test timed out — too many connections to test at once",
|
||||
"advancedSettings": "Advanced Settings",
|
||||
"chatPathLabel": "Chat Endpoint Path",
|
||||
"chatPathPlaceholder": "/chat/completions",
|
||||
"chatPathHint": "Custom chat path for providers with non-standard APIs (e.g. /v4/chat/completions)",
|
||||
"modelsPathLabel": "Models Endpoint Path",
|
||||
"modelsPathPlaceholder": "/models",
|
||||
"modelsPathHint": "Custom models path for validation (e.g. /v4/models)"
|
||||
},
|
||||
"settings": {
|
||||
"title": "הגדרות",
|
||||
|
||||
@@ -1386,10 +1386,17 @@
|
||||
"failed": "Sikertelen",
|
||||
"leaveBlankKeepCurrentApiKey": "Hagyja üresen az aktuális API-kulcs megtartásához.",
|
||||
"editCompatibleTitle": "Szerkesztés {type} Kompatibilis",
|
||||
"compatibleBaseUrlHint": "Használja a {type}-kompatibilis API alap URL-jét (a /v1 végződésű).",
|
||||
"compatibleBaseUrlHint": "Root URL of your {type}-compatible API. Use Advanced Settings for custom endpoint paths.",
|
||||
"apiKeyForCheck": "API-kulcs (ellenőrzéshez)",
|
||||
"compatibleProdPlaceholder": "{type} Kompatibilis (termék)",
|
||||
"providerTestTimeout": "Provider test timed out — too many connections to test at once"
|
||||
"providerTestTimeout": "Provider test timed out — too many connections to test at once",
|
||||
"advancedSettings": "Advanced Settings",
|
||||
"chatPathLabel": "Chat Endpoint Path",
|
||||
"chatPathPlaceholder": "/chat/completions",
|
||||
"chatPathHint": "Custom chat path for providers with non-standard APIs (e.g. /v4/chat/completions)",
|
||||
"modelsPathLabel": "Models Endpoint Path",
|
||||
"modelsPathPlaceholder": "/models",
|
||||
"modelsPathHint": "Custom models path for validation (e.g. /v4/models)"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Beállítások elemre",
|
||||
|
||||
@@ -1386,10 +1386,17 @@
|
||||
"failed": "Gagal",
|
||||
"leaveBlankKeepCurrentApiKey": "Biarkan kosong untuk mempertahankan kunci API saat ini.",
|
||||
"editCompatibleTitle": "Sunting {type} Kompatibel",
|
||||
"compatibleBaseUrlHint": "Gunakan URL dasar (berakhiran /v1) untuk API Anda yang kompatibel dengan {type}.",
|
||||
"compatibleBaseUrlHint": "Root URL of your {type}-compatible API. Use Advanced Settings for custom endpoint paths.",
|
||||
"apiKeyForCheck": "Kunci API (untuk Pemeriksaan)",
|
||||
"compatibleProdPlaceholder": "{type} Kompatibel (Prod)",
|
||||
"providerTestTimeout": "Provider test timed out — too many connections to test at once"
|
||||
"providerTestTimeout": "Provider test timed out — too many connections to test at once",
|
||||
"advancedSettings": "Advanced Settings",
|
||||
"chatPathLabel": "Chat Endpoint Path",
|
||||
"chatPathPlaceholder": "/chat/completions",
|
||||
"chatPathHint": "Custom chat path for providers with non-standard APIs (e.g. /v4/chat/completions)",
|
||||
"modelsPathLabel": "Models Endpoint Path",
|
||||
"modelsPathPlaceholder": "/models",
|
||||
"modelsPathHint": "Custom models path for validation (e.g. /v4/models)"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Pengaturan",
|
||||
|
||||
@@ -1386,10 +1386,17 @@
|
||||
"failed": "असफल",
|
||||
"leaveBlankKeepCurrentApiKey": "वर्तमान एपीआई कुंजी रखने के लिए खाली छोड़ दें।",
|
||||
"editCompatibleTitle": "संपादित करें {type} संगत",
|
||||
"compatibleBaseUrlHint": "अपने {type}-संगत API के लिए आधार URL (/v1 पर समाप्त) का उपयोग करें।",
|
||||
"compatibleBaseUrlHint": "Root URL of your {type}-compatible API. Use Advanced Settings for custom endpoint paths.",
|
||||
"apiKeyForCheck": "एपीआई कुंजी (चेक के लिए)",
|
||||
"compatibleProdPlaceholder": "{type} संगत (उत्पाद)",
|
||||
"providerTestTimeout": "Provider test timed out — too many connections to test at once"
|
||||
"providerTestTimeout": "Provider test timed out — too many connections to test at once",
|
||||
"advancedSettings": "Advanced Settings",
|
||||
"chatPathLabel": "Chat Endpoint Path",
|
||||
"chatPathPlaceholder": "/chat/completions",
|
||||
"chatPathHint": "Custom chat path for providers with non-standard APIs (e.g. /v4/chat/completions)",
|
||||
"modelsPathLabel": "Models Endpoint Path",
|
||||
"modelsPathPlaceholder": "/models",
|
||||
"modelsPathHint": "Custom models path for validation (e.g. /v4/models)"
|
||||
},
|
||||
"settings": {
|
||||
"title": "सेटिंग्स",
|
||||
|
||||
@@ -1386,10 +1386,17 @@
|
||||
"failed": "Fallito",
|
||||
"leaveBlankKeepCurrentApiKey": "Lascia vuoto per mantenere la chiave API corrente.",
|
||||
"editCompatibleTitle": "Modifica {type} Compatibile",
|
||||
"compatibleBaseUrlHint": "Utilizza l'URL di base (che termina con /v1) per la tua API compatibile con {type}.",
|
||||
"compatibleBaseUrlHint": "Root URL of your {type}-compatible API. Use Advanced Settings for custom endpoint paths.",
|
||||
"apiKeyForCheck": "Chiave API (per controllo)",
|
||||
"compatibleProdPlaceholder": "{type} Compatibile (prodotto)",
|
||||
"providerTestTimeout": "Provider test timed out — too many connections to test at once"
|
||||
"providerTestTimeout": "Provider test timed out — too many connections to test at once",
|
||||
"advancedSettings": "Advanced Settings",
|
||||
"chatPathLabel": "Chat Endpoint Path",
|
||||
"chatPathPlaceholder": "/chat/completions",
|
||||
"chatPathHint": "Custom chat path for providers with non-standard APIs (e.g. /v4/chat/completions)",
|
||||
"modelsPathLabel": "Models Endpoint Path",
|
||||
"modelsPathPlaceholder": "/models",
|
||||
"modelsPathHint": "Custom models path for validation (e.g. /v4/models)"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Impostazioni",
|
||||
|
||||
@@ -1386,10 +1386,17 @@
|
||||
"failed": "失敗しました",
|
||||
"leaveBlankKeepCurrentApiKey": "現在の API キーを保持するには、空白のままにします。",
|
||||
"editCompatibleTitle": "編集 {type} 互換",
|
||||
"compatibleBaseUrlHint": "{type} 互換 API のベース URL (/v1 で終わる) を使用します。",
|
||||
"compatibleBaseUrlHint": "Root URL of your {type}-compatible API. Use Advanced Settings for custom endpoint paths.",
|
||||
"apiKeyForCheck": "APIキー(チェック用)",
|
||||
"compatibleProdPlaceholder": "{type} 互換性あり (製品)",
|
||||
"providerTestTimeout": "Provider test timed out — too many connections to test at once"
|
||||
"providerTestTimeout": "Provider test timed out — too many connections to test at once",
|
||||
"advancedSettings": "Advanced Settings",
|
||||
"chatPathLabel": "Chat Endpoint Path",
|
||||
"chatPathPlaceholder": "/chat/completions",
|
||||
"chatPathHint": "Custom chat path for providers with non-standard APIs (e.g. /v4/chat/completions)",
|
||||
"modelsPathLabel": "Models Endpoint Path",
|
||||
"modelsPathPlaceholder": "/models",
|
||||
"modelsPathHint": "Custom models path for validation (e.g. /v4/models)"
|
||||
},
|
||||
"settings": {
|
||||
"title": "設定",
|
||||
|
||||
@@ -1386,10 +1386,17 @@
|
||||
"failed": "실패",
|
||||
"leaveBlankKeepCurrentApiKey": "현재 API 키를 유지하려면 비워 두세요.",
|
||||
"editCompatibleTitle": "{type} 호환 가능 편집",
|
||||
"compatibleBaseUrlHint": "{type} 호환 API에는 기본 URL(/v1로 끝남)을 사용하세요.",
|
||||
"compatibleBaseUrlHint": "Root URL of your {type}-compatible API. Use Advanced Settings for custom endpoint paths.",
|
||||
"apiKeyForCheck": "API Key(확인용)",
|
||||
"compatibleProdPlaceholder": "{type} 호환 가능(프로덕션)",
|
||||
"providerTestTimeout": "Provider test timed out — too many connections to test at once"
|
||||
"providerTestTimeout": "Provider test timed out — too many connections to test at once",
|
||||
"advancedSettings": "Advanced Settings",
|
||||
"chatPathLabel": "Chat Endpoint Path",
|
||||
"chatPathPlaceholder": "/chat/completions",
|
||||
"chatPathHint": "Custom chat path for providers with non-standard APIs (e.g. /v4/chat/completions)",
|
||||
"modelsPathLabel": "Models Endpoint Path",
|
||||
"modelsPathPlaceholder": "/models",
|
||||
"modelsPathHint": "Custom models path for validation (e.g. /v4/models)"
|
||||
},
|
||||
"settings": {
|
||||
"title": "설정",
|
||||
|
||||
@@ -1386,10 +1386,17 @@
|
||||
"failed": "gagal",
|
||||
"leaveBlankKeepCurrentApiKey": "Biarkan kosong untuk mengekalkan kunci API semasa.",
|
||||
"editCompatibleTitle": "Edit {type} Serasi",
|
||||
"compatibleBaseUrlHint": "Gunakan URL asas (berakhir dengan /v1) untuk API serasi {type} anda.",
|
||||
"compatibleBaseUrlHint": "Root URL of your {type}-compatible API. Use Advanced Settings for custom endpoint paths.",
|
||||
"apiKeyForCheck": "Kunci API (untuk Semakan)",
|
||||
"compatibleProdPlaceholder": "{type} Serasi (Prod)",
|
||||
"providerTestTimeout": "Provider test timed out — too many connections to test at once"
|
||||
"providerTestTimeout": "Provider test timed out — too many connections to test at once",
|
||||
"advancedSettings": "Advanced Settings",
|
||||
"chatPathLabel": "Chat Endpoint Path",
|
||||
"chatPathPlaceholder": "/chat/completions",
|
||||
"chatPathHint": "Custom chat path for providers with non-standard APIs (e.g. /v4/chat/completions)",
|
||||
"modelsPathLabel": "Models Endpoint Path",
|
||||
"modelsPathPlaceholder": "/models",
|
||||
"modelsPathHint": "Custom models path for validation (e.g. /v4/models)"
|
||||
},
|
||||
"settings": {
|
||||
"title": "tetapan",
|
||||
|
||||
@@ -1386,10 +1386,17 @@
|
||||
"failed": "Mislukt",
|
||||
"leaveBlankKeepCurrentApiKey": "Laat dit leeg om de huidige API-sleutel te behouden.",
|
||||
"editCompatibleTitle": "Bewerk {type} Compatibel",
|
||||
"compatibleBaseUrlHint": "Gebruik de basis-URL (eindigend op /v1) voor uw {type}-compatibele API.",
|
||||
"compatibleBaseUrlHint": "Root URL of your {type}-compatible API. Use Advanced Settings for custom endpoint paths.",
|
||||
"apiKeyForCheck": "API-sleutel (ter controle)",
|
||||
"compatibleProdPlaceholder": "{type} Compatibel (product)",
|
||||
"providerTestTimeout": "Provider test timed out — too many connections to test at once"
|
||||
"providerTestTimeout": "Provider test timed out — too many connections to test at once",
|
||||
"advancedSettings": "Advanced Settings",
|
||||
"chatPathLabel": "Chat Endpoint Path",
|
||||
"chatPathPlaceholder": "/chat/completions",
|
||||
"chatPathHint": "Custom chat path for providers with non-standard APIs (e.g. /v4/chat/completions)",
|
||||
"modelsPathLabel": "Models Endpoint Path",
|
||||
"modelsPathPlaceholder": "/models",
|
||||
"modelsPathHint": "Custom models path for validation (e.g. /v4/models)"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Instellingen",
|
||||
|
||||
@@ -1386,10 +1386,17 @@
|
||||
"failed": "Mislyktes",
|
||||
"leaveBlankKeepCurrentApiKey": "La stå tomt for å beholde gjeldende API-nøkkel.",
|
||||
"editCompatibleTitle": "Rediger {type} Kompatibel",
|
||||
"compatibleBaseUrlHint": "Bruk basis-URLen (som slutter på /v1) for din {type}-kompatible API.",
|
||||
"compatibleBaseUrlHint": "Root URL of your {type}-compatible API. Use Advanced Settings for custom endpoint paths.",
|
||||
"apiKeyForCheck": "API-nøkkel (for sjekk)",
|
||||
"compatibleProdPlaceholder": "{type} Kompatibel (Prod)",
|
||||
"providerTestTimeout": "Provider test timed out — too many connections to test at once"
|
||||
"providerTestTimeout": "Provider test timed out — too many connections to test at once",
|
||||
"advancedSettings": "Advanced Settings",
|
||||
"chatPathLabel": "Chat Endpoint Path",
|
||||
"chatPathPlaceholder": "/chat/completions",
|
||||
"chatPathHint": "Custom chat path for providers with non-standard APIs (e.g. /v4/chat/completions)",
|
||||
"modelsPathLabel": "Models Endpoint Path",
|
||||
"modelsPathPlaceholder": "/models",
|
||||
"modelsPathHint": "Custom models path for validation (e.g. /v4/models)"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Innstillinger",
|
||||
|
||||
@@ -1386,10 +1386,17 @@
|
||||
"failed": "Nabigo",
|
||||
"leaveBlankKeepCurrentApiKey": "Iwanang blangko upang mapanatili ang kasalukuyang API key.",
|
||||
"editCompatibleTitle": "I-edit ang {type} Compatible",
|
||||
"compatibleBaseUrlHint": "Gamitin ang base URL (nagtatapos sa /v1) para sa iyong {type}-compatible na API.",
|
||||
"compatibleBaseUrlHint": "Root URL of your {type}-compatible API. Use Advanced Settings for custom endpoint paths.",
|
||||
"apiKeyForCheck": "API Key (para sa Pagsusuri)",
|
||||
"compatibleProdPlaceholder": "{type} Compatible (Prod)",
|
||||
"providerTestTimeout": "Provider test timed out — too many connections to test at once"
|
||||
"providerTestTimeout": "Provider test timed out — too many connections to test at once",
|
||||
"advancedSettings": "Advanced Settings",
|
||||
"chatPathLabel": "Chat Endpoint Path",
|
||||
"chatPathPlaceholder": "/chat/completions",
|
||||
"chatPathHint": "Custom chat path for providers with non-standard APIs (e.g. /v4/chat/completions)",
|
||||
"modelsPathLabel": "Models Endpoint Path",
|
||||
"modelsPathPlaceholder": "/models",
|
||||
"modelsPathHint": "Custom models path for validation (e.g. /v4/models)"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Mga setting",
|
||||
|
||||
@@ -1386,10 +1386,17 @@
|
||||
"failed": "Nie udało się",
|
||||
"leaveBlankKeepCurrentApiKey": "Pozostaw puste, aby zachować bieżący klucz API.",
|
||||
"editCompatibleTitle": "Edytuj {type} Kompatybilny",
|
||||
"compatibleBaseUrlHint": "Użyj podstawowego adresu URL (kończącego się na /v1) dla interfejsu API zgodnego z {type}.",
|
||||
"compatibleBaseUrlHint": "Root URL of your {type}-compatible API. Use Advanced Settings for custom endpoint paths.",
|
||||
"apiKeyForCheck": "Klucz API (do sprawdzenia)",
|
||||
"compatibleProdPlaceholder": "{type} Kompatybilny (Prod)",
|
||||
"providerTestTimeout": "Provider test timed out — too many connections to test at once"
|
||||
"providerTestTimeout": "Provider test timed out — too many connections to test at once",
|
||||
"advancedSettings": "Advanced Settings",
|
||||
"chatPathLabel": "Chat Endpoint Path",
|
||||
"chatPathPlaceholder": "/chat/completions",
|
||||
"chatPathHint": "Custom chat path for providers with non-standard APIs (e.g. /v4/chat/completions)",
|
||||
"modelsPathLabel": "Models Endpoint Path",
|
||||
"modelsPathPlaceholder": "/models",
|
||||
"modelsPathHint": "Custom models path for validation (e.g. /v4/models)"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Ustawienia",
|
||||
|
||||
@@ -1419,10 +1419,17 @@
|
||||
"failed": "Falhou",
|
||||
"leaveBlankKeepCurrentApiKey": "Deixe em branco para manter a chave de API atual.",
|
||||
"editCompatibleTitle": "Editar Compatível {type}",
|
||||
"compatibleBaseUrlHint": "Use a URL base (terminando em /v1) para sua API compatível com {type}.",
|
||||
"compatibleBaseUrlHint": "Root URL of your {type}-compatible API. Use Advanced Settings for custom endpoint paths.",
|
||||
"apiKeyForCheck": "Chave de API (para verificação)",
|
||||
"compatibleProdPlaceholder": "{type} Compatível (Prod)",
|
||||
"providerTestTimeout": "Provider test timed out — too many connections to test at once"
|
||||
"providerTestTimeout": "Provider test timed out — too many connections to test at once",
|
||||
"advancedSettings": "Advanced Settings",
|
||||
"chatPathLabel": "Chat Endpoint Path",
|
||||
"chatPathPlaceholder": "/chat/completions",
|
||||
"chatPathHint": "Custom chat path for providers with non-standard APIs (e.g. /v4/chat/completions)",
|
||||
"modelsPathLabel": "Models Endpoint Path",
|
||||
"modelsPathPlaceholder": "/models",
|
||||
"modelsPathHint": "Custom models path for validation (e.g. /v4/models)"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Configurações",
|
||||
|
||||
@@ -1398,10 +1398,17 @@
|
||||
"failed": "Falha",
|
||||
"leaveBlankKeepCurrentApiKey": "Deixe em branco para manter a chave API atual.",
|
||||
"editCompatibleTitle": "Editar {type} Compatível",
|
||||
"compatibleBaseUrlHint": "Use o URL base (terminando em /v1) para sua API compatível com {type}.",
|
||||
"compatibleBaseUrlHint": "Root URL of your {type}-compatible API. Use Advanced Settings for custom endpoint paths.",
|
||||
"apiKeyForCheck": "Chave API (para verificação)",
|
||||
"compatibleProdPlaceholder": "{type} Compatível (Produção)",
|
||||
"providerTestTimeout": "Provider test timed out — too many connections to test at once"
|
||||
"providerTestTimeout": "Provider test timed out — too many connections to test at once",
|
||||
"advancedSettings": "Advanced Settings",
|
||||
"chatPathLabel": "Chat Endpoint Path",
|
||||
"chatPathPlaceholder": "/chat/completions",
|
||||
"chatPathHint": "Custom chat path for providers with non-standard APIs (e.g. /v4/chat/completions)",
|
||||
"modelsPathLabel": "Models Endpoint Path",
|
||||
"modelsPathPlaceholder": "/models",
|
||||
"modelsPathHint": "Custom models path for validation (e.g. /v4/models)"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Configurações",
|
||||
|
||||
@@ -1386,10 +1386,17 @@
|
||||
"failed": "A eșuat",
|
||||
"leaveBlankKeepCurrentApiKey": "Lăsați necompletat pentru a păstra cheia API curentă.",
|
||||
"editCompatibleTitle": "Editați {type} Compatibil",
|
||||
"compatibleBaseUrlHint": "Utilizați adresa URL de bază (se termină în /v1) pentru API-ul dvs. compatibil {type}.",
|
||||
"compatibleBaseUrlHint": "Root URL of your {type}-compatible API. Use Advanced Settings for custom endpoint paths.",
|
||||
"apiKeyForCheck": "Cheie API (pentru verificare)",
|
||||
"compatibleProdPlaceholder": "{type} Compatibil (Prod)",
|
||||
"providerTestTimeout": "Provider test timed out — too many connections to test at once"
|
||||
"providerTestTimeout": "Provider test timed out — too many connections to test at once",
|
||||
"advancedSettings": "Advanced Settings",
|
||||
"chatPathLabel": "Chat Endpoint Path",
|
||||
"chatPathPlaceholder": "/chat/completions",
|
||||
"chatPathHint": "Custom chat path for providers with non-standard APIs (e.g. /v4/chat/completions)",
|
||||
"modelsPathLabel": "Models Endpoint Path",
|
||||
"modelsPathPlaceholder": "/models",
|
||||
"modelsPathHint": "Custom models path for validation (e.g. /v4/models)"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Setări",
|
||||
|
||||
@@ -1386,10 +1386,17 @@
|
||||
"failed": "Не удалось",
|
||||
"leaveBlankKeepCurrentApiKey": "Оставьте пустым, чтобы сохранить текущий ключ API.",
|
||||
"editCompatibleTitle": "Изменить совместимость {type}",
|
||||
"compatibleBaseUrlHint": "Используйте базовый URL-адрес (оканчивающийся на /v1) для вашего {type}-совместимого API.",
|
||||
"compatibleBaseUrlHint": "Root URL of your {type}-compatible API. Use Advanced Settings for custom endpoint paths.",
|
||||
"apiKeyForCheck": "API-ключ (для проверки)",
|
||||
"compatibleProdPlaceholder": "{type} Совместимость (Прод.)",
|
||||
"providerTestTimeout": "Provider test timed out — too many connections to test at once"
|
||||
"providerTestTimeout": "Provider test timed out — too many connections to test at once",
|
||||
"advancedSettings": "Advanced Settings",
|
||||
"chatPathLabel": "Chat Endpoint Path",
|
||||
"chatPathPlaceholder": "/chat/completions",
|
||||
"chatPathHint": "Custom chat path for providers with non-standard APIs (e.g. /v4/chat/completions)",
|
||||
"modelsPathLabel": "Models Endpoint Path",
|
||||
"modelsPathPlaceholder": "/models",
|
||||
"modelsPathHint": "Custom models path for validation (e.g. /v4/models)"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Настройки",
|
||||
|
||||
@@ -1386,10 +1386,17 @@
|
||||
"failed": "Nepodarilo sa",
|
||||
"leaveBlankKeepCurrentApiKey": "Ak chcete zachovať aktuálny kľúč API, nechajte pole prázdne.",
|
||||
"editCompatibleTitle": "Upraviť {type} kompatibilné",
|
||||
"compatibleBaseUrlHint": "Pre svoje {type}-kompatibilné API použite základnú webovú adresu (končiacu na /v1).",
|
||||
"compatibleBaseUrlHint": "Root URL of your {type}-compatible API. Use Advanced Settings for custom endpoint paths.",
|
||||
"apiKeyForCheck": "API kľúč (na kontrolu)",
|
||||
"compatibleProdPlaceholder": "{type} Kompatibilné (produkt)",
|
||||
"providerTestTimeout": "Provider test timed out — too many connections to test at once"
|
||||
"providerTestTimeout": "Provider test timed out — too many connections to test at once",
|
||||
"advancedSettings": "Advanced Settings",
|
||||
"chatPathLabel": "Chat Endpoint Path",
|
||||
"chatPathPlaceholder": "/chat/completions",
|
||||
"chatPathHint": "Custom chat path for providers with non-standard APIs (e.g. /v4/chat/completions)",
|
||||
"modelsPathLabel": "Models Endpoint Path",
|
||||
"modelsPathPlaceholder": "/models",
|
||||
"modelsPathHint": "Custom models path for validation (e.g. /v4/models)"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Nastavenia",
|
||||
|
||||
@@ -1386,10 +1386,17 @@
|
||||
"failed": "Misslyckades",
|
||||
"leaveBlankKeepCurrentApiKey": "Lämna tomt för att behålla den aktuella API-nyckeln.",
|
||||
"editCompatibleTitle": "Redigera {type} Kompatibel",
|
||||
"compatibleBaseUrlHint": "Använd basadressen (som slutar på /v1) för ditt {type}-kompatibla API.",
|
||||
"compatibleBaseUrlHint": "Root URL of your {type}-compatible API. Use Advanced Settings for custom endpoint paths.",
|
||||
"apiKeyForCheck": "API-nyckel (för kontroll)",
|
||||
"compatibleProdPlaceholder": "{type} Kompatibel (Prod)",
|
||||
"providerTestTimeout": "Provider test timed out — too many connections to test at once"
|
||||
"providerTestTimeout": "Provider test timed out — too many connections to test at once",
|
||||
"advancedSettings": "Advanced Settings",
|
||||
"chatPathLabel": "Chat Endpoint Path",
|
||||
"chatPathPlaceholder": "/chat/completions",
|
||||
"chatPathHint": "Custom chat path for providers with non-standard APIs (e.g. /v4/chat/completions)",
|
||||
"modelsPathLabel": "Models Endpoint Path",
|
||||
"modelsPathPlaceholder": "/models",
|
||||
"modelsPathHint": "Custom models path for validation (e.g. /v4/models)"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Inställningar",
|
||||
|
||||
@@ -1386,10 +1386,17 @@
|
||||
"failed": "ล้มเหลว",
|
||||
"leaveBlankKeepCurrentApiKey": "เว้นว่างไว้เพื่อเก็บคีย์ API ปัจจุบันไว้",
|
||||
"editCompatibleTitle": "แก้ไข {type} เข้ากันได้",
|
||||
"compatibleBaseUrlHint": "ใช้ URL พื้นฐาน (ลงท้ายด้วย /v1) สำหรับ {type}- API ที่เข้ากันได้กับของคุณ",
|
||||
"compatibleBaseUrlHint": "Root URL of your {type}-compatible API. Use Advanced Settings for custom endpoint paths.",
|
||||
"apiKeyForCheck": "คีย์ API (สำหรับการตรวจสอบ)",
|
||||
"compatibleProdPlaceholder": "{type} เข้ากันได้ (ผลิตภัณฑ์)",
|
||||
"providerTestTimeout": "Provider test timed out — too many connections to test at once"
|
||||
"providerTestTimeout": "Provider test timed out — too many connections to test at once",
|
||||
"advancedSettings": "Advanced Settings",
|
||||
"chatPathLabel": "Chat Endpoint Path",
|
||||
"chatPathPlaceholder": "/chat/completions",
|
||||
"chatPathHint": "Custom chat path for providers with non-standard APIs (e.g. /v4/chat/completions)",
|
||||
"modelsPathLabel": "Models Endpoint Path",
|
||||
"modelsPathPlaceholder": "/models",
|
||||
"modelsPathHint": "Custom models path for validation (e.g. /v4/models)"
|
||||
},
|
||||
"settings": {
|
||||
"title": "การตั้งค่า",
|
||||
|
||||
@@ -1386,10 +1386,17 @@
|
||||
"failed": "Не вдалося",
|
||||
"leaveBlankKeepCurrentApiKey": "Залиште поле порожнім, щоб зберегти поточний ключ API.",
|
||||
"editCompatibleTitle": "Редагувати {type} Сумісний",
|
||||
"compatibleBaseUrlHint": "Використовуйте базову URL-адресу (закінчується на /v1) для свого {type}-сумісного API.",
|
||||
"compatibleBaseUrlHint": "Root URL of your {type}-compatible API. Use Advanced Settings for custom endpoint paths.",
|
||||
"apiKeyForCheck": "Ключ API (для перевірки)",
|
||||
"compatibleProdPlaceholder": "{type} Сумісність (Prod)",
|
||||
"providerTestTimeout": "Provider test timed out — too many connections to test at once"
|
||||
"providerTestTimeout": "Provider test timed out — too many connections to test at once",
|
||||
"advancedSettings": "Advanced Settings",
|
||||
"chatPathLabel": "Chat Endpoint Path",
|
||||
"chatPathPlaceholder": "/chat/completions",
|
||||
"chatPathHint": "Custom chat path for providers with non-standard APIs (e.g. /v4/chat/completions)",
|
||||
"modelsPathLabel": "Models Endpoint Path",
|
||||
"modelsPathPlaceholder": "/models",
|
||||
"modelsPathHint": "Custom models path for validation (e.g. /v4/models)"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Налаштування",
|
||||
|
||||
@@ -1386,10 +1386,17 @@
|
||||
"failed": "thất bại",
|
||||
"leaveBlankKeepCurrentApiKey": "Để trống để giữ khóa API hiện tại.",
|
||||
"editCompatibleTitle": "Chỉnh sửa {type} Tương thích",
|
||||
"compatibleBaseUrlHint": "Sử dụng URL cơ sở (kết thúc bằng /v1) cho API tương thích {type} của bạn.",
|
||||
"compatibleBaseUrlHint": "Root URL of your {type}-compatible API. Use Advanced Settings for custom endpoint paths.",
|
||||
"apiKeyForCheck": "Khóa API (để kiểm tra)",
|
||||
"compatibleProdPlaceholder": "{type} Tương thích (Sản phẩm)",
|
||||
"providerTestTimeout": "Provider test timed out — too many connections to test at once"
|
||||
"providerTestTimeout": "Provider test timed out — too many connections to test at once",
|
||||
"advancedSettings": "Advanced Settings",
|
||||
"chatPathLabel": "Chat Endpoint Path",
|
||||
"chatPathPlaceholder": "/chat/completions",
|
||||
"chatPathHint": "Custom chat path for providers with non-standard APIs (e.g. /v4/chat/completions)",
|
||||
"modelsPathLabel": "Models Endpoint Path",
|
||||
"modelsPathPlaceholder": "/models",
|
||||
"modelsPathHint": "Custom models path for validation (e.g. /v4/models)"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Cài đặt",
|
||||
|
||||
@@ -1386,10 +1386,17 @@
|
||||
"failed": "失败",
|
||||
"leaveBlankKeepCurrentApiKey": "留空以保留当前的 API 密钥。",
|
||||
"editCompatibleTitle": "编辑 {type} 兼容",
|
||||
"compatibleBaseUrlHint": "使用 {type} 兼容 API 的基本 URL(以 /v1 结尾)。",
|
||||
"compatibleBaseUrlHint": "Root URL of your {type}-compatible API. Use Advanced Settings for custom endpoint paths.",
|
||||
"apiKeyForCheck": "API 密钥(用于检查)",
|
||||
"compatibleProdPlaceholder": "{type} 兼容(产品)",
|
||||
"providerTestTimeout": "Provider test timed out — too many connections to test at once"
|
||||
"providerTestTimeout": "Provider test timed out — too many connections to test at once",
|
||||
"advancedSettings": "Advanced Settings",
|
||||
"chatPathLabel": "Chat Endpoint Path",
|
||||
"chatPathPlaceholder": "/chat/completions",
|
||||
"chatPathHint": "Custom chat path for providers with non-standard APIs (e.g. /v4/chat/completions)",
|
||||
"modelsPathLabel": "Models Endpoint Path",
|
||||
"modelsPathPlaceholder": "/models",
|
||||
"modelsPathHint": "Custom models path for validation (e.g. /v4/models)"
|
||||
},
|
||||
"settings": {
|
||||
"title": "设置",
|
||||
|
||||
5
src/lib/db/migrations/003_provider_node_custom_paths.sql
Normal file
5
src/lib/db/migrations/003_provider_node_custom_paths.sql
Normal file
@@ -0,0 +1,5 @@
|
||||
-- Add custom endpoint path columns to provider_nodes
|
||||
-- Allows compatible providers to override default chat/models paths
|
||||
-- NULL = use default path (backward compatible)
|
||||
ALTER TABLE provider_nodes ADD COLUMN chat_path TEXT;
|
||||
ALTER TABLE provider_nodes ADD COLUMN models_path TEXT;
|
||||
@@ -453,14 +453,16 @@ export async function createProviderNode(data: JsonRecord) {
|
||||
prefix: data.prefix || null,
|
||||
apiType: data.apiType || null,
|
||||
baseUrl: data.baseUrl || null,
|
||||
chatPath: data.chatPath || null,
|
||||
modelsPath: data.modelsPath || null,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
|
||||
db.prepare(
|
||||
`
|
||||
INSERT INTO provider_nodes (id, type, name, prefix, api_type, base_url, created_at, updated_at)
|
||||
VALUES (@id, @type, @name, @prefix, @apiType, @baseUrl, @createdAt, @updatedAt)
|
||||
INSERT INTO provider_nodes (id, type, name, prefix, api_type, base_url, chat_path, models_path, created_at, updated_at)
|
||||
VALUES (@id, @type, @name, @prefix, @apiType, @baseUrl, @chatPath, @modelsPath, @createdAt, @updatedAt)
|
||||
`
|
||||
).run(node);
|
||||
|
||||
@@ -482,7 +484,8 @@ export async function updateProviderNode(id: string, data: JsonRecord) {
|
||||
db.prepare(
|
||||
`
|
||||
UPDATE provider_nodes SET type = @type, name = @name, prefix = @prefix,
|
||||
api_type = @apiType, base_url = @baseUrl, updated_at = @updatedAt
|
||||
api_type = @apiType, base_url = @baseUrl, chat_path = @chatPath,
|
||||
models_path = @modelsPath, updated_at = @updatedAt
|
||||
WHERE id = @id
|
||||
`
|
||||
).run({
|
||||
@@ -492,6 +495,8 @@ export async function updateProviderNode(id: string, data: JsonRecord) {
|
||||
prefix: merged["prefix"] || null,
|
||||
apiType: merged["apiType"] || null,
|
||||
baseUrl: merged["baseUrl"] || null,
|
||||
chatPath: merged["chatPath"] || null,
|
||||
modelsPath: merged["modelsPath"] || null,
|
||||
updatedAt: merged["updatedAt"],
|
||||
});
|
||||
|
||||
|
||||
@@ -819,6 +819,8 @@ export const createProviderNodeSchema = z
|
||||
apiType: z.enum(["chat", "responses"]).optional(),
|
||||
baseUrl: z.string().trim().min(1).optional(),
|
||||
type: z.enum(["openai-compatible", "anthropic-compatible"]).optional(),
|
||||
chatPath: z.string().trim().startsWith("/").max(500).optional().or(z.literal("")),
|
||||
modelsPath: z.string().trim().startsWith("/").max(500).optional().or(z.literal("")),
|
||||
})
|
||||
.superRefine((value, ctx) => {
|
||||
const nodeType = value.type || "openai-compatible";
|
||||
@@ -836,12 +838,15 @@ export const updateProviderNodeSchema = z.object({
|
||||
prefix: z.string().trim().min(1, "Prefix is required"),
|
||||
apiType: z.enum(["chat", "responses"]).optional(),
|
||||
baseUrl: z.string().trim().min(1, "Base URL is required"),
|
||||
chatPath: z.string().trim().startsWith("/").max(500).optional().or(z.literal("")),
|
||||
modelsPath: z.string().trim().startsWith("/").max(500).optional().or(z.literal("")),
|
||||
});
|
||||
|
||||
export const providerNodeValidateSchema = z.object({
|
||||
baseUrl: z.string().trim().min(1, "Base URL and API key required"),
|
||||
apiKey: z.string().trim().min(1, "Base URL and API key required"),
|
||||
type: z.enum(["openai-compatible", "anthropic-compatible"]).optional(),
|
||||
modelsPath: z.string().trim().startsWith("/").max(500).optional().or(z.literal("")),
|
||||
});
|
||||
|
||||
export const updateProviderConnectionSchema = z
|
||||
|
||||
140
tests/unit/custom-endpoint-paths.test.mjs
Normal file
140
tests/unit/custom-endpoint-paths.test.mjs
Normal file
@@ -0,0 +1,140 @@
|
||||
import { describe, it } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
// Inline buildUrl logic from DefaultExecutor for unit testing
|
||||
// (avoids importing ESM modules with complex dependency chains)
|
||||
|
||||
function buildUrlOpenAI(provider, credentials) {
|
||||
const psd = credentials?.providerSpecificData;
|
||||
const baseUrl = psd?.baseUrl || "https://api.openai.com/v1";
|
||||
const normalized = baseUrl.replace(/\/$/, "");
|
||||
const customPath = typeof psd?.chatPath === "string" && psd.chatPath ? psd.chatPath : null;
|
||||
if (customPath) return `${normalized}${customPath}`;
|
||||
const path = provider.includes("responses") ? "/responses" : "/chat/completions";
|
||||
return `${normalized}${path}`;
|
||||
}
|
||||
|
||||
function buildUrlAnthropic(credentials) {
|
||||
const psd = credentials?.providerSpecificData;
|
||||
const baseUrl = psd?.baseUrl || "https://api.anthropic.com/v1";
|
||||
const normalized = baseUrl.replace(/\/$/, "");
|
||||
const customPath = typeof psd?.chatPath === "string" && psd.chatPath ? psd.chatPath : null;
|
||||
return `${normalized}${customPath || "/messages"}`;
|
||||
}
|
||||
|
||||
function buildModelsUrl(baseUrl, modelsPath) {
|
||||
const normalized = baseUrl.replace(/\/$/, "");
|
||||
return `${normalized}${modelsPath || "/models"}`;
|
||||
}
|
||||
|
||||
describe("Custom Endpoint Paths", () => {
|
||||
describe("OpenAI Compatible buildUrl", () => {
|
||||
it("returns custom chatPath when provided", () => {
|
||||
const url = buildUrlOpenAI("openai-compatible-chat-abc123", {
|
||||
providerSpecificData: {
|
||||
baseUrl: "https://api.epsiloncode.pl",
|
||||
chatPath: "/v4/chat/completions",
|
||||
},
|
||||
});
|
||||
assert.equal(url, "https://api.epsiloncode.pl/v4/chat/completions");
|
||||
});
|
||||
|
||||
it("returns default /chat/completions without chatPath", () => {
|
||||
const url = buildUrlOpenAI("openai-compatible-chat-abc123", {
|
||||
providerSpecificData: {
|
||||
baseUrl: "https://api.openai.com/v1",
|
||||
},
|
||||
});
|
||||
assert.equal(url, "https://api.openai.com/v1/chat/completions");
|
||||
});
|
||||
|
||||
it("returns /responses for responses provider without chatPath", () => {
|
||||
const url = buildUrlOpenAI("openai-compatible-responses-abc123", {
|
||||
providerSpecificData: {
|
||||
baseUrl: "https://api.openai.com/v1",
|
||||
},
|
||||
});
|
||||
assert.equal(url, "https://api.openai.com/v1/responses");
|
||||
});
|
||||
|
||||
it("treats empty string chatPath as default", () => {
|
||||
const url = buildUrlOpenAI("openai-compatible-chat-abc123", {
|
||||
providerSpecificData: {
|
||||
baseUrl: "https://api.example.com/v1",
|
||||
chatPath: "",
|
||||
},
|
||||
});
|
||||
assert.equal(url, "https://api.example.com/v1/chat/completions");
|
||||
});
|
||||
|
||||
it("strips trailing slash from baseUrl", () => {
|
||||
const url = buildUrlOpenAI("openai-compatible-chat-abc123", {
|
||||
providerSpecificData: {
|
||||
baseUrl: "https://api.example.com/v1/",
|
||||
chatPath: "/v4/chat/completions",
|
||||
},
|
||||
});
|
||||
assert.equal(url, "https://api.example.com/v1/v4/chat/completions");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Anthropic Compatible buildUrl", () => {
|
||||
it("returns custom chatPath when provided", () => {
|
||||
const url = buildUrlAnthropic({
|
||||
providerSpecificData: {
|
||||
baseUrl: "https://proxy.example.com/v2",
|
||||
chatPath: "/v4/messages",
|
||||
},
|
||||
});
|
||||
assert.equal(url, "https://proxy.example.com/v2/v4/messages");
|
||||
});
|
||||
|
||||
it("returns default /messages without chatPath", () => {
|
||||
const url = buildUrlAnthropic({
|
||||
providerSpecificData: {
|
||||
baseUrl: "https://api.anthropic.com/v1",
|
||||
},
|
||||
});
|
||||
assert.equal(url, "https://api.anthropic.com/v1/messages");
|
||||
});
|
||||
|
||||
it("treats empty string chatPath as default", () => {
|
||||
const url = buildUrlAnthropic({
|
||||
providerSpecificData: {
|
||||
baseUrl: "https://api.anthropic.com/v1",
|
||||
chatPath: "",
|
||||
},
|
||||
});
|
||||
assert.equal(url, "https://api.anthropic.com/v1/messages");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Validate endpoint modelsPath", () => {
|
||||
it("uses modelsPath when provided", () => {
|
||||
const url = buildModelsUrl("https://api.example.com/v1", "/v4/models");
|
||||
assert.equal(url, "https://api.example.com/v1/v4/models");
|
||||
});
|
||||
|
||||
it("falls back to /models when modelsPath is empty", () => {
|
||||
const url = buildModelsUrl("https://api.example.com/v1", "");
|
||||
assert.equal(url, "https://api.example.com/v1/models");
|
||||
});
|
||||
|
||||
it("falls back to /models when modelsPath is undefined", () => {
|
||||
const url = buildModelsUrl("https://api.example.com/v1", undefined);
|
||||
assert.equal(url, "https://api.example.com/v1/models");
|
||||
});
|
||||
});
|
||||
|
||||
describe("No credentials fallback", () => {
|
||||
it("works with null credentials for openai-compatible", () => {
|
||||
const url = buildUrlOpenAI("openai-compatible-chat-abc123", null);
|
||||
assert.equal(url, "https://api.openai.com/v1/chat/completions");
|
||||
});
|
||||
|
||||
it("works with null credentials for anthropic-compatible", () => {
|
||||
const url = buildUrlAnthropic(null);
|
||||
assert.equal(url, "https://api.anthropic.com/v1/messages");
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user