mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-02 05:12:11 +03:00
Allow provider_nodes to configure custom chat and models endpoint paths via chatPath/modelsPath fields. This enables providers with non-standard versioned APIs (e.g. /v4/chat/completions) to work without embedding the version prefix in base_url. - Add migration 003: chat_path and models_path columns - Update Zod schemas (create, update, validate) - Update CRUD in providers.ts (INSERT/UPDATE) - Wire chatPath/modelsPath through API routes and providerSpecificData cascade - Read chatPath in DefaultExecutor and BaseExecutor buildUrl() - Use modelsPath in validate endpoint - Add Advanced Settings UI section (collapsible) in create/edit modals - Update base URL hint to reference Advanced Settings - Add i18n keys across all 30 locales - Add unit tests for buildUrl with custom paths Backward compatible: NULL chatPath/modelsPath = default behavior.
113 lines
4.1 KiB
TypeScript
113 lines
4.1 KiB
TypeScript
import { BaseExecutor } from "./base.ts";
|
|
import { PROVIDERS, OAUTH_ENDPOINTS } from "../config/constants.ts";
|
|
import { getAccessToken } from "../services/tokenRefresh.ts";
|
|
|
|
export class DefaultExecutor extends BaseExecutor {
|
|
constructor(provider) {
|
|
super(provider, PROVIDERS[provider] || PROVIDERS.openai);
|
|
}
|
|
|
|
buildUrl(model, stream, urlIndex = 0, credentials = null) {
|
|
if (this.provider?.startsWith?.("openai-compatible-")) {
|
|
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 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"}`;
|
|
}
|
|
switch (this.provider) {
|
|
case "claude":
|
|
case "glm":
|
|
case "kimi-coding":
|
|
case "minimax":
|
|
case "minimax-cn":
|
|
return `${this.config.baseUrl}?beta=true`;
|
|
case "gemini":
|
|
return `${this.config.baseUrl}/${model}:${stream ? "streamGenerateContent?alt=sse" : "generateContent"}`;
|
|
default:
|
|
return this.config.baseUrl;
|
|
}
|
|
}
|
|
|
|
buildHeaders(credentials, stream = true) {
|
|
const headers = { "Content-Type": "application/json", ...this.config.headers };
|
|
|
|
switch (this.provider) {
|
|
case "gemini":
|
|
credentials.apiKey
|
|
? (headers["x-goog-api-key"] = credentials.apiKey)
|
|
: (headers["Authorization"] = `Bearer ${credentials.accessToken}`);
|
|
break;
|
|
case "claude":
|
|
credentials.apiKey
|
|
? (headers["x-api-key"] = credentials.apiKey)
|
|
: (headers["Authorization"] = `Bearer ${credentials.accessToken}`);
|
|
break;
|
|
case "glm":
|
|
case "kimi-coding":
|
|
case "minimax":
|
|
case "minimax-cn":
|
|
headers["x-api-key"] = credentials.apiKey || credentials.accessToken;
|
|
break;
|
|
default:
|
|
if (this.provider?.startsWith?.("anthropic-compatible-")) {
|
|
if (credentials.apiKey) {
|
|
headers["x-api-key"] = credentials.apiKey;
|
|
} else if (credentials.accessToken) {
|
|
headers["Authorization"] = `Bearer ${credentials.accessToken}`;
|
|
}
|
|
if (!headers["anthropic-version"]) {
|
|
headers["anthropic-version"] = "2023-06-01";
|
|
}
|
|
} else {
|
|
headers["Authorization"] = `Bearer ${credentials.apiKey || credentials.accessToken}`;
|
|
}
|
|
}
|
|
|
|
if (stream) headers["Accept"] = "text/event-stream";
|
|
return headers;
|
|
}
|
|
|
|
/**
|
|
* For compatible providers, ensure the model name sent upstream
|
|
* is the clean model name without internal routing prefixes.
|
|
* e.g. "openapi-chat-anti/claude-opus-4-6-thinking" → "claude-opus-4-6-thinking"
|
|
*/
|
|
transformRequest(model, body, stream, credentials) {
|
|
if (
|
|
this.provider?.startsWith?.("openai-compatible-") ||
|
|
this.provider?.startsWith?.("anthropic-compatible-")
|
|
) {
|
|
const cleanModel = model.includes("/") ? model.split("/").slice(1).join("/") : model;
|
|
return { ...body, model: cleanModel };
|
|
}
|
|
return body;
|
|
}
|
|
|
|
/**
|
|
* Refresh credentials via the centralized tokenRefresh service.
|
|
* Delegates to getAccessToken() which handles all providers with
|
|
* race-condition protection (deduplication via refreshPromiseCache).
|
|
*/
|
|
async refreshCredentials(credentials, log) {
|
|
if (!credentials.refreshToken) return null;
|
|
try {
|
|
return await getAccessToken(this.provider, credentials, log);
|
|
} catch (error) {
|
|
log?.error?.("TOKEN", `${this.provider} refresh error: ${error.message}`);
|
|
return null;
|
|
}
|
|
}
|
|
}
|
|
|
|
export default DefaultExecutor;
|