diff --git a/open-sse/config/providerModels.js b/open-sse/config/providerModels.js index ca4a3ee..12b08de 100644 --- a/open-sse/config/providerModels.js +++ b/open-sse/config/providerModels.js @@ -197,7 +197,6 @@ export const PROVIDER_MODELS = { { id: "gemini-3.1-flash-image-preview", name: "Gemini 3.1 Flash Image Preview" }, // Gemini 3 series { id: "gemini-3-flash-preview", name: "Gemini 3 Flash Preview" }, - { id: "gemini-3-flash-lite-preview", name: "Gemini 3 Flash Lite Preview" }, // Gemini 2.5 series { id: "gemini-2.5-pro", name: "Gemini 2.5 Pro" }, { id: "gemini-2.5-flash", name: "Gemini 2.5 Flash" }, @@ -215,11 +214,13 @@ export const PROVIDER_MODELS = { // { id: "openrouter/free", name: "Free Models (Auto)" }, ], glm: [ + { id: "glm-5.1", name: "GLM 5.1" }, { id: "glm-5", name: "GLM 5" }, { id: "glm-4.7", name: "GLM 4.7" }, { id: "glm-4.6v", name: "GLM 4.6V (Vision)" }, ], "glm-cn": [ + { id: "glm-5.1", name: "GLM 5.1" }, { id: "glm-5", name: "GLM 5" }, { id: "glm-4.7", name: "GLM-4.7" }, { id: "glm-4.6", name: "GLM-4.6" }, @@ -311,10 +312,6 @@ export const PROVIDER_MODELS = { nvidia: [ { id: "moonshotai/kimi-k2.5", name: "Kimi K2.5" }, { id: "z-ai/glm4.7", name: "GLM 4.7" }, - { id: "deepseek-ai/deepseek-v3.2", name: "DeepSeek V3.2" }, - { id: "nvidia/llama-3.3-70b-instruct", name: "Llama 3.3 70B" }, - { id: "meta/llama-4-maverick-17b-128e-instruct", name: "Llama 4 Maverick" }, - { id: "deepseek/deepseek-r1", name: "DeepSeek R1" }, ], nebius: [ { id: "meta-llama/Llama-3.3-70B-Instruct", name: "Llama 3.3 70B Instruct" }, diff --git a/open-sse/translator/request/openai-to-gemini.js b/open-sse/translator/request/openai-to-gemini.js index 3f2be95..463d114 100644 --- a/open-sse/translator/request/openai-to-gemini.js +++ b/open-sse/translator/request/openai-to-gemini.js @@ -1,9 +1,16 @@ import { register } from "../index.js"; import { FORMATS } from "../formats.js"; -// import { DEFAULT_THINKING_GEMINI_SIGNATURE } from "../../config/defaultThinkingSignature.js"; import { ANTIGRAVITY_DEFAULT_SYSTEM } from "../../config/appConstants.js"; import { openaiToClaudeRequestForAntigravity } from "./openai-to-claude.js"; +// Decode base64url → standard base64 (for restoring thoughtSignature) +function fromBase64Url(b64url) { + let b64 = b64url.replace(/-/g, "+").replace(/_/g, "/"); + // Restore padding + while (b64.length % 4) b64 += "="; + return b64; +} + function generateUUID() { return crypto.randomUUID(); } @@ -58,26 +65,32 @@ function openaiToGeminiBase(model, body, stream) { result.generationConfig.maxOutputTokens = body.max_tokens; } - // Build tool_call_id -> name map + // Strip embedded thoughtSignature from a tool_call id ("rawId_TSIG_sig" → "rawId") + const stripSig = (id) => { + const sep = id ? id.indexOf("_TSIG_") : -1; + return sep !== -1 ? id.slice(0, sep) : id; + }; + + // Build tool_call_id -> name map (keyed by rawId) const tcID2Name = {}; if (body.messages && Array.isArray(body.messages)) { for (const msg of body.messages) { if (msg.role === "assistant" && msg.tool_calls) { for (const tc of msg.tool_calls) { if (tc.type === "function" && tc.id && tc.function?.name) { - tcID2Name[tc.id] = tc.function.name; + tcID2Name[stripSig(tc.id)] = tc.function.name; } } } } } - // Build tool responses cache + // Build tool responses cache (keyed by rawId) const toolResponses = {}; if (body.messages && Array.isArray(body.messages)) { for (const msg of body.messages) { if (msg.role === "tool" && msg.tool_call_id) { - toolResponses[msg.tool_call_id] = msg.content; + toolResponses[stripSig(msg.tool_call_id)] = msg.content; } } } @@ -102,18 +115,6 @@ function openaiToGeminiBase(model, body, stream) { } else if (role === "assistant") { const parts = []; - // Thinking/reasoning → thought part - if (msg.reasoning_content) { - parts.push({ - thought: true, - text: msg.reasoning_content - }); - // parts.push({ - // thoughtSignature: DEFAULT_THINKING_GEMINI_SIGNATURE, - // text: "" - // }); - } - if (content) { const text = typeof content === "string" ? content : extractTextContent(content); if (text) { @@ -127,15 +128,22 @@ function openaiToGeminiBase(model, body, stream) { if (tc.type !== "function") continue; const args = tryParseJSON(tc.function?.arguments || "{}"); - parts.push({ - // thoughtSignature: DEFAULT_THINKING_GEMINI_SIGNATURE, + // Extract thoughtSignature embedded in ID as "rawId_TSIG_base64urlsig" + const sepIdx = tc.id.indexOf("_TSIG_"); + const rawId = sepIdx !== -1 ? tc.id.slice(0, sepIdx) : tc.id; + const encodedSig = sepIdx !== -1 ? tc.id.slice(sepIdx + 6) : ""; + const fcPart = { functionCall: { - id: tc.id, + id: rawId, name: sanitizeGeminiFunctionName(tc.function.name), args: args } - }); - toolCallIds.push(tc.id); + }; + if (encodedSig) { + fcPart.thoughtSignature = fromBase64Url(encodedSig); + } + parts.push(fcPart); + toolCallIds.push(rawId); } if (parts.length > 0) { diff --git a/open-sse/translator/response/gemini-to-openai.js b/open-sse/translator/response/gemini-to-openai.js index 7512396..d54213e 100644 --- a/open-sse/translator/response/gemini-to-openai.js +++ b/open-sse/translator/response/gemini-to-openai.js @@ -1,6 +1,11 @@ import { register } from "../index.js"; import { FORMATS } from "../formats.js"; +// Encode base64 → base64url (safe for tool_call IDs: only [a-zA-Z0-9_-]) +function toBase64Url(b64) { + return b64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, ""); +} + // Convert Gemini response chunk to OpenAI format export function geminiToOpenAIResponse(chunk, state) { if (!chunk) return null; @@ -18,6 +23,7 @@ export function geminiToOpenAIResponse(chunk, state) { state.messageId = response.responseId || `msg_${Date.now()}`; state.model = response.modelVersion || "gemini"; state.functionIndex = 0; + state.pendingThoughtSignature = null; results.push({ id: `chatcmpl-${state.messageId}`, object: "chat.completion.chunk", @@ -34,66 +40,19 @@ export function geminiToOpenAIResponse(chunk, state) { // Process parts if (content?.parts) { for (const part of content.parts) { - const hasThoughtSig = part.thoughtSignature || part.thought_signature; + const partThoughtSig = part.thoughtSignature || part.thought_signature || ""; const isThought = part.thought === true; - - // Handle thought signature (thinking mode) - if (hasThoughtSig) { - const hasTextContent = part.text !== undefined && part.text !== ""; - const hasFunctionCall = !!part.functionCall; - - if (hasTextContent) { - results.push({ - id: `chatcmpl-${state.messageId}`, - object: "chat.completion.chunk", - created: Math.floor(Date.now() / 1000), - model: state.model, - choices: [{ - index: 0, - delta: isThought - ? { reasoning_content: part.text } - : { content: part.text }, - finish_reason: null - }] - }); - } - - if (hasFunctionCall) { - const rawName = part.functionCall.name; - // Restore original tool name from mapping (AG cloaking) - const fcName = state.toolNameMap?.get(rawName) || rawName; - const fcArgs = part.functionCall.args || {}; - const toolCallIndex = state.functionIndex++; - - const toolCall = { - id: `${fcName}-${Date.now()}-${toolCallIndex}`, - index: toolCallIndex, - type: "function", - function: { - name: fcName, - arguments: JSON.stringify(fcArgs) - } - }; - - state.toolCalls.set(toolCallIndex, toolCall); - - results.push({ - id: `chatcmpl-${state.messageId}`, - object: "chat.completion.chunk", - created: Math.floor(Date.now() / 1000), - model: state.model, - choices: [{ - index: 0, - delta: { tool_calls: [toolCall] }, - finish_reason: null - }] - }); - } - continue; + + // Accumulate thoughtSignature across parts: a sig-only part precedes the functionCall part + if (partThoughtSig) { + state.pendingThoughtSignature = partThoughtSig; } - // Text content (non-thinking) - if (part.text !== undefined && part.text !== "") { + const hasTextContent = part.text !== undefined && part.text !== ""; + const hasFunctionCall = !!part.functionCall; + + // Emit reasoning/thought text + if (hasTextContent) { results.push({ id: `chatcmpl-${state.messageId}`, object: "chat.completion.chunk", @@ -101,22 +60,29 @@ export function geminiToOpenAIResponse(chunk, state) { model: state.model, choices: [{ index: 0, - delta: { content: part.text }, + delta: isThought + ? { reasoning_content: part.text } + : { content: part.text }, finish_reason: null }] }); } - // Function call - if (part.functionCall) { - const rawName = part.functionCall.name; - // Restore original tool name from mapping (AG cloaking) - const fcName = state.toolNameMap?.get(rawName) || rawName; + // Emit function call, attaching the best available thoughtSignature + if (hasFunctionCall) { + const fcName = part.functionCall.name; const fcArgs = part.functionCall.args || {}; const toolCallIndex = state.functionIndex++; - + // Use signature from this part, or the one carried from a preceding part + const thoughtSig = partThoughtSig || state.pendingThoughtSignature || ""; + if (thoughtSig) state.pendingThoughtSignature = null; // consumed + // Encode signature using _TSIG_ delimiter and base64url for safe tool_call ID + const toolCallId = thoughtSig + ? `${fcName}-${Date.now()}-${toolCallIndex}_TSIG_${toBase64Url(thoughtSig)}` + : `${fcName}-${Date.now()}-${toolCallIndex}`; + const toolCall = { - id: `${fcName}-${Date.now()}-${toolCallIndex}`, + id: toolCallId, index: toolCallIndex, type: "function", function: { @@ -124,9 +90,9 @@ export function geminiToOpenAIResponse(chunk, state) { arguments: JSON.stringify(fcArgs) } }; - + state.toolCalls.set(toolCallIndex, toolCall); - + results.push({ id: `chatcmpl-${state.messageId}`, object: "chat.completion.chunk", diff --git a/src/app/api/models/test/route.js b/src/app/api/models/test/route.js index 69c5475..b24a0dd 100644 --- a/src/app/api/models/test/route.js +++ b/src/app/api/models/test/route.js @@ -7,8 +7,8 @@ export async function POST(request) { const { model } = await request.json(); if (!model) return NextResponse.json({ error: "Model required" }, { status: 400 }); - const url = new URL(request.url); - const baseUrl = `${url.protocol}//${url.host}`; + const baseUrl = process.env.BASE_URL || + (() => { const u = new URL(request.url); return `${u.protocol}//${u.host}`; })(); // Get an active internal API key for auth (if requireApiKey is enabled) let apiKey = null; diff --git a/src/lib/localDb.js b/src/lib/localDb.js index 6756976..f911a14 100644 --- a/src/lib/localDb.js +++ b/src/lib/localDb.js @@ -40,11 +40,6 @@ if (!isCloud && !fs.existsSync(DATA_DIR)) { fs.mkdirSync(DATA_DIR, { recursive: true }); } -// Seed db.json with defaults on first run so proper-lockfile never hits ENOENT -if (!isCloud && DB_FILE && !fs.existsSync(DB_FILE)) { - fs.writeFileSync(DB_FILE, JSON.stringify(defaultData, null, 2)); -} - // Default data structure const defaultData = { providerConnections: [], @@ -63,7 +58,7 @@ const defaultData = { comboStrategy: "fallback", comboStrategies: {}, requireLogin: true, - enableObservability: false, + observabilityEnabled: true, observabilityMaxRecords: 1000, observabilityBatchSize: 20, observabilityFlushIntervalMs: 5000, @@ -75,6 +70,11 @@ const defaultData = { pricing: {} // NEW: pricing configuration }; +// Seed db.json with defaults on first run so proper-lockfile never hits ENOENT +if (!isCloud && DB_FILE && !fs.existsSync(DB_FILE)) { + fs.writeFileSync(DB_FILE, JSON.stringify(defaultData, null, 2)); +} + function cloneDefaultData() { return { providerConnections: [], @@ -93,7 +93,7 @@ function cloneDefaultData() { comboStrategy: "fallback", comboStrategies: {}, requireLogin: true, - enableObservability: false, + observabilityEnabled: true, observabilityMaxRecords: 1000, observabilityBatchSize: 20, observabilityFlushIntervalMs: 5000, @@ -167,31 +167,50 @@ function ensureDbShape(data) { // Singleton instance let dbInstance = null; -// In-memory read cache to avoid redundant disk reads under high load -const DB_CACHE_TTL = 500; // ms -let dbCache = { data: null, ts: 0 }; - -// Serialize all DB operations (reads on cache-miss + writes) to prevent race conditions -let dbQueue = Promise.resolve(); - -function withDbLock(fn) { - const next = dbQueue.then(fn, fn); - dbQueue = next.catch(() => {}); - return next; -} - -// Lock options for proper-lockfile +// Lock options for proper-lockfile (increased retries for multi-process robustness) const LOCK_OPTIONS = { retries: { - retries: 5, - minTimeout: 100, - maxTimeout: 2000, + retries: 15, + minTimeout: 50, + maxTimeout: 3000, }, stale: 10000, // Consider lock stale after 10s }; +// In-process mutex to serialize DB access within the same process +// This prevents ELOCKED when concurrent requests in the same process try to acquire the file lock +class LocalMutex { + constructor() { + this._queue = []; + this._locked = false; + } + + async acquire() { + if (!this._locked) { + this._locked = true; + return () => this._release(); + } + return new Promise((resolve) => { + this._queue.push(resolve); + }).then(() => () => this._release()); + } + + _release() { + const next = this._queue.shift(); + if (next) { + next(); + } else { + this._locked = false; + } + } +} + +// Singleton local mutex for in-process serialization +const localMutex = new LocalMutex(); + /** * Safely read database with file locking + * Uses local mutex first to serialize within-process access, then file lock for cross-process */ async function safeRead(db) { if (isCloud) { @@ -199,9 +218,11 @@ async function safeRead(db) { return; } + // Acquire local mutex first (in-process serialization) + const releaseLocal = await localMutex.acquire(); let release = null; try { - // Acquire lock before reading + // Acquire file lock (cross-process serialization) release = await lockfile.lock(DB_FILE, LOCK_OPTIONS); await db.read(); } catch (error) { @@ -218,12 +239,13 @@ async function safeRead(db) { // Ignore unlock errors } } + releaseLocal(); } } /** - * Safely write database with file locking. - * Always invalidates read cache so next read reflects the latest state. + * Safely write database with file locking + * Uses local mutex first to serialize within-process access, then file lock for cross-process */ async function safeWrite(db) { if (isCloud) { @@ -231,12 +253,13 @@ async function safeWrite(db) { return; } + // Acquire local mutex first (in-process serialization) + const releaseLocal = await localMutex.acquire(); let release = null; try { + // Acquire file lock (cross-process serialization) release = await lockfile.lock(DB_FILE, LOCK_OPTIONS); await db.write(); - // Invalidate cache immediately after a successful write - dbCache.ts = 0; } catch (error) { if (error.code === "ELOCKED") { console.warn("[DB] File is locked, retrying write..."); @@ -251,77 +274,55 @@ async function safeWrite(db) { // Ignore unlock errors } } + releaseLocal(); } } /** - * Get database instance (singleton). - * - * Hot path: if cache is fresh, return immediately without any I/O or queuing. - * Cold path: serialize via withDbLock to prevent concurrent reads from racing - * against in-flight writes (eliminates lost-update race condition). + * Get database instance (singleton) */ export async function getDb() { if (isCloud) { + // Return in-memory DB for Workers if (!dbInstance) { const data = cloneDefaultData(); - dbInstance = new Low({ read: async () => {}, write: async () => {} }, data); + dbInstance = new Low({ read: async () => { }, write: async () => { } }, data); dbInstance.data = data; } return dbInstance; } - // Hot path: cache hit — no lock, no disk I/O - if (dbCache.data && Date.now() - dbCache.ts < DB_CACHE_TTL) { - if (!dbInstance) { - const adapter = new JSONFile(DB_FILE); - dbInstance = new Low(adapter, dbCache.data); - } - dbInstance.data = dbCache.data; - return dbInstance; + if (!dbInstance) { + const adapter = new JSONFile(DB_FILE); + dbInstance = new Low(adapter, cloneDefaultData()); } - // Cold path: serialize with writes to prevent race conditions - return withDbLock(async () => { - // Re-check cache inside lock — another queued task may have already loaded it - if (dbCache.data && Date.now() - dbCache.ts < DB_CACHE_TTL) { - dbInstance.data = dbCache.data; - return dbInstance; - } - - if (!dbInstance) { - const adapter = new JSONFile(DB_FILE); - dbInstance = new Low(adapter, cloneDefaultData()); - } - - try { - await safeRead(dbInstance); - } catch (error) { - if (error instanceof SyntaxError) { - console.warn("[DB] Corrupt JSON detected, resetting to defaults..."); - dbInstance.data = cloneDefaultData(); - await safeWrite(dbInstance); - } else { - throw error; - } - } - - // Initialize/migrate missing keys for older DB schema versions - if (!dbInstance.data) { + // Always read latest disk state to avoid stale singleton data across route workers. + try { + await safeRead(dbInstance); + } catch (error) { + if (error instanceof SyntaxError) { + console.warn('[DB] Corrupt JSON detected, resetting to defaults...'); dbInstance.data = cloneDefaultData(); await safeWrite(dbInstance); } else { - const { data, changed } = ensureDbShape(dbInstance.data); - dbInstance.data = data; - if (changed) await safeWrite(dbInstance); + throw error; } + } - // Update cache after successful read - dbCache.data = dbInstance.data; - dbCache.ts = Date.now(); + // Initialize/migrate missing keys for older DB schema versions. + if (!dbInstance.data) { + dbInstance.data = cloneDefaultData(); + await safeWrite(dbInstance); + } else { + const { data, changed } = ensureDbShape(dbInstance.data); + dbInstance.data = data; + if (changed) { + await safeWrite(dbInstance); + } + } - return dbInstance; - }); + return dbInstance; } // ============ Provider Connections ============ @@ -332,17 +333,17 @@ export async function getDb() { export async function getProviderConnections(filter = {}) { const db = await getDb(); let connections = db.data.providerConnections || []; - + if (filter.provider) { connections = connections.filter(c => c.provider === filter.provider); } if (filter.isActive !== undefined) { connections = connections.filter(c => c.isActive === filter.isActive); } - + // Sort by priority (lower = higher priority) connections.sort((a, b) => (a.priority || 999) - (b.priority || 999)); - + return connections; } @@ -375,12 +376,12 @@ export async function getProviderNodeById(id) { */ export async function createProviderNode(data) { const db = await getDb(); - + // Initialize providerNodes if undefined (backward compatibility) if (!db.data.providerNodes) { db.data.providerNodes = []; } - + const now = new Date().toISOString(); const node = { @@ -408,7 +409,7 @@ export async function updateProviderNode(id, data) { if (!db.data.providerNodes) { db.data.providerNodes = []; } - + const index = db.data.providerNodes.findIndex((node) => node.id === id); if (index === -1) return null; @@ -569,7 +570,7 @@ export async function getProviderConnectionById(id) { export async function createProviderConnection(data) { const db = await getDb(); const now = new Date().toISOString(); - + // Check for existing connection with same provider and email (for OAuth) // or same provider and name (for API key) let existingIndex = -1; @@ -582,7 +583,7 @@ export async function createProviderConnection(data) { c => c.provider === data.provider && c.authType === "apikey" && c.name === data.name ); } - + // If exists, update instead of create if (existingIndex !== -1) { db.data.providerConnections[existingIndex] = { @@ -593,7 +594,7 @@ export async function createProviderConnection(data) { await safeWrite(db); return db.data.providerConnections[existingIndex]; } - + // Generate name for OAuth if not provided let connectionName = data.name || null; if (!connectionName && data.authType === "oauth") { @@ -617,7 +618,7 @@ export async function createProviderConnection(data) { const maxPriority = providerConnections.reduce((max, c) => Math.max(max, c.priority || 0), 0); connectionPriority = maxPriority + 1; } - + // Create new connection - only save fields with actual values const connection = { id: uuidv4(), @@ -638,7 +639,7 @@ export async function createProviderConnection(data) { "lastTested", "lastError", "lastErrorAt", "rateLimitedUntil", "expiresIn", "errorCode", "consecutiveUseCount" ]; - + for (const field of optionalFields) { if (data[field] !== undefined && data[field] !== null) { connection[field] = data[field]; @@ -649,9 +650,12 @@ export async function createProviderConnection(data) { if (data.providerSpecificData && Object.keys(data.providerSpecificData).length > 0) { connection.providerSpecificData = data.providerSpecificData; } - + db.data.providerConnections.push(connection); - await reorderProviderConnections(data.provider, db); + await safeWrite(db); + + // Reorder to ensure consistency + await reorderProviderConnections(data.provider); return connection; } @@ -673,11 +677,11 @@ export async function updateProviderConnection(id, data) { updatedAt: new Date().toISOString(), }; - // Reorder if priority was changed, reuse same db instance to avoid double-read + await safeWrite(db); + + // Reorder if priority was changed if (data.priority !== undefined) { - await reorderProviderConnections(providerId, db); - } else { - await safeWrite(db); + await reorderProviderConnections(providerId); } return db.data.providerConnections[index]; @@ -695,35 +699,37 @@ export async function deleteProviderConnection(id) { const providerId = db.data.providerConnections[index].provider; db.data.providerConnections.splice(index, 1); - // Reorder to fill gaps, reuse same db instance to avoid double-read - await reorderProviderConnections(providerId, db); + await safeWrite(db); + + // Reorder to fill gaps + await reorderProviderConnections(providerId); return true; } /** - * Reorder provider connections to ensure unique, sequential priorities. - * Accepts an existing db instance to avoid redundant getDb() calls and - * prevent double-read race conditions within the same write operation. + * Reorder provider connections to ensure unique, sequential priorities */ -export async function reorderProviderConnections(providerId, db) { - const instance = db || (await getDb()); - if (!instance.data.providerConnections) return; +export async function reorderProviderConnections(providerId) { + const db = await getDb(); + if (!db.data.providerConnections) return; - const providerConnections = instance.data.providerConnections + const providerConnections = db.data.providerConnections .filter(c => c.provider === providerId) .sort((a, b) => { + // Sort by priority first const pDiff = (a.priority || 0) - (b.priority || 0); if (pDiff !== 0) return pDiff; // Use updatedAt as tie-breaker (newer first) return new Date(b.updatedAt || 0) - new Date(a.updatedAt || 0); }); + // Re-assign sequential priorities providerConnections.forEach((conn, index) => { conn.priority = index + 1; }); - await safeWrite(instance); + await safeWrite(db); } // ============ Model Aliases ============ @@ -802,7 +808,7 @@ export async function getComboByName(name) { export async function createCombo(data) { const db = await getDb(); if (!db.data.combos) db.data.combos = []; - + const now = new Date().toISOString(); const combo = { id: uuidv4(), @@ -811,7 +817,7 @@ export async function createCombo(data) { createdAt: now, updatedAt: now, }; - + db.data.combos.push(combo); await safeWrite(db); return combo; @@ -823,16 +829,16 @@ export async function createCombo(data) { export async function updateCombo(id, data) { const db = await getDb(); if (!db.data.combos) db.data.combos = []; - + const index = db.data.combos.findIndex(c => c.id === id); if (index === -1) return null; - + db.data.combos[index] = { ...db.data.combos[index], ...data, updatedAt: new Date().toISOString(), }; - + await safeWrite(db); return db.data.combos[index]; } @@ -843,10 +849,10 @@ export async function updateCombo(id, data) { export async function deleteCombo(id) { const db = await getDb(); if (!db.data.combos) return false; - + const index = db.data.combos.findIndex(c => c.id === id); if (index === -1) return false; - + db.data.combos.splice(index, 1); await safeWrite(db); return true; @@ -883,14 +889,14 @@ export async function createApiKey(name, machineId) { if (!machineId) { throw new Error("machineId is required"); } - + const db = await getDb(); const now = new Date().toISOString(); - + // Always use new format: sk-{machineId}-{keyId}-{crc8} const { generateApiKeyWithMachine } = await import("@/shared/utils/apiKey"); const result = generateApiKeyWithMachine(machineId); - + const apiKey = { id: uuidv4(), name: name, @@ -899,10 +905,10 @@ export async function createApiKey(name, machineId) { isActive: true, createdAt: now, }; - + db.data.apiKeys.push(apiKey); await safeWrite(db); - + return apiKey; } @@ -912,12 +918,12 @@ export async function createApiKey(name, machineId) { export async function deleteApiKey(id) { const db = await getDb(); const index = db.data.apiKeys.findIndex(k => k.id === id); - + if (index === -1) return false; - + db.data.apiKeys.splice(index, 1); await safeWrite(db); - + return true; }