feat: Add OpenAI-compatible provider nodes

- Support multiple OpenAI-compatible providers with custom prefix/baseUrl
- Add provider nodes CRUD (create/read/update/delete)
- URL building: baseUrl + /chat/completions or /responses
- Model import from /models endpoint
- API key validation via /models
- Usage type safety across all translators
- OAuth token auto-refresh for expired tokens
This commit is contained in:
decolua
2026-02-02 19:45:12 +07:00
parent 1b14c9d66b
commit 0a28f9f924
25 changed files with 1276 additions and 151 deletions

View File

@@ -189,10 +189,10 @@ function translateNonStreamingResponse(responseBody, targetFormat, sourceFormat)
* Handles different provider response formats
*/
function extractUsageFromResponse(responseBody, provider) {
if (!responseBody) return null;
if (!responseBody || typeof responseBody !== 'object') return null;
// OpenAI format
if (responseBody.usage) {
if (responseBody.usage && typeof responseBody.usage === 'object') {
return {
prompt_tokens: responseBody.usage.prompt_tokens || 0,
completion_tokens: responseBody.usage.completion_tokens || 0,
@@ -202,7 +202,7 @@ function extractUsageFromResponse(responseBody, provider) {
}
// Claude format
if (responseBody.usage?.input_tokens !== undefined || responseBody.usage?.output_tokens !== undefined) {
if (responseBody.usage && typeof responseBody.usage === 'object' && (responseBody.usage.input_tokens !== undefined || responseBody.usage.output_tokens !== undefined)) {
return {
prompt_tokens: responseBody.usage.input_tokens || 0,
completion_tokens: responseBody.usage.output_tokens || 0,
@@ -212,7 +212,7 @@ function extractUsageFromResponse(responseBody, provider) {
}
// Gemini format
if (responseBody.usageMetadata) {
if (responseBody.usageMetadata && typeof responseBody.usageMetadata === 'object') {
return {
prompt_tokens: responseBody.usageMetadata.promptTokenCount || 0,
completion_tokens: responseBody.usageMetadata.candidatesTokenCount || 0,
@@ -411,11 +411,11 @@ export async function handleChatCore({ body, modelInfo, credentials, log, onCred
await onRequestSuccess();
}
// Log usage for non-streaming responses
// Log usage for non-streaming responses
const usage = extractUsageFromResponse(responseBody, provider);
appendRequestLog({ model, provider, connectionId, tokens: usage, status: "200 OK" }).catch(() => { });
if (usage) {
const msg = `[${new Date().toLocaleTimeString("en-US", { hour12: false, hour: "2-digit", minute: "2-digit" })}] 📊 [USAGE] ${provider.toUpperCase()} | in=${usage.prompt_tokens || 0} | out=${usage.completion_tokens || 0}${connectionId ? ` | account=${connectionId.slice(0, 8)}...` : ""}`;
if (usage && typeof usage === 'object') {
const msg = `[${new Date().toLocaleTimeString("en-US", { hour12: false, hour: "2-digit", minute: "2-digit" })}] 📊 [USAGE] ${provider.toUpperCase()} | in=${usage?.prompt_tokens || 0} | out=${usage?.completion_tokens || 0}${connectionId ? ` | account=${connectionId.slice(0, 8)}...` : ""}`;
console.log(`${COLORS.green}${msg}${COLORS.reset}`);
saveRequestUsage({