From b7446f8dd1667d92f31f3e352b56a2175d7c0c05 Mon Sep 17 00:00:00 2001 From: decolua Date: Tue, 22 Sep 2026 09:51:29 +0700 Subject: [PATCH] feat(providers): add System One (Jev) decision endpoint New /v1/systemone pass-through route for Jev decision models (jev-1.13, jev-1.13-free) on OpenCode Zen and the free lane. Follows the media-route pattern: systemoneConfig in the registry drives URL/headers, the handler mirrors the embeddings account-fallback + usage flow, and the dashboard gains a System One media-provider kind. No chat-pipeline changes. Co-Authored-By: Claude Code --- next.config.mjs | 4 + open-sse/handlers/systemoneCore.js | 95 ++++++++++++ open-sse/providers/index.js | 2 +- open-sse/providers/registry/opencode-zen.js | 1 - open-sse/providers/registry/opencode.js | 9 ++ src/app/api/v1/systemone/route.js | 21 +++ src/shared/constants/providers.js | 3 +- src/sse/handlers/systemone.js | 152 ++++++++++++++++++++ 8 files changed, 284 insertions(+), 3 deletions(-) create mode 100644 open-sse/handlers/systemoneCore.js create mode 100644 src/app/api/v1/systemone/route.js create mode 100644 src/sse/handlers/systemone.js diff --git a/next.config.mjs b/next.config.mjs index ecd385ca..517986c9 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -75,6 +75,10 @@ const nextConfig = { source: "/responses", destination: "/api/v1/responses" }, + { + source: "/systemone", + destination: "/api/v1/systemone" + }, { source: "/v1beta/:path*", destination: "/api/v1beta/:path*" diff --git a/open-sse/handlers/systemoneCore.js b/open-sse/handlers/systemoneCore.js new file mode 100644 index 00000000..82da570d --- /dev/null +++ b/open-sse/handlers/systemoneCore.js @@ -0,0 +1,95 @@ +import { createErrorResult, parseUpstreamError, formatProviderError } from "../utils/error.js"; +import { HTTP_STATUS, FETCH_CONNECT_TIMEOUT_MS } from "../config/runtimeConfig.js"; +import { PROVIDER_MEDIA } from "../providers/index.js"; +import { generateSessionId } from "../executors/opencode-zen.js"; + +/** + * Core System One (Jev) handler — native decision payload pass-through. + * URL/headers come from the registry's systemoneConfig; body and JSON response + * are forwarded untouched (decision models have no chat translation layer). + * + * @returns {Promise<{ success: boolean, response: Response, usage?: object, status?: number, error?: string }>} + */ +export async function handleSystemoneCore({ + body, + modelInfo, + credentials, + log, + onRequestSuccess, +}) { + const { provider, model } = modelInfo; + const cfg = PROVIDER_MEDIA[provider]?.systemoneConfig; + if (!cfg?.baseUrl) { + return createErrorResult( + HTTP_STATUS.BAD_REQUEST, + `Provider '${provider}' does not support System One.` + ); + } + + // Validate input at the trust boundary; question-level shape is upstream's job. + if (body.state === undefined || body.state === null) { + return createErrorResult(HTTP_STATUS.BAD_REQUEST, "Missing required field: state"); + } + if (!body.questions || typeof body.questions !== "object" || Array.isArray(body.questions)) { + return createErrorResult(HTTP_STATUS.BAD_REQUEST, "Missing required field: questions"); + } + + // noAuth free lanes carry accessToken "public" from the credential stub. + const token = credentials?.apiKey || credentials?.accessToken; + const headers = { + "Content-Type": "application/json", + ...(token ? { Authorization: `Bearer ${token}` } : {}), + ...(cfg.headers || {}), + // Zen lanes expect the official client session header on every request. + "x-opencode-session": generateSessionId(), + }; + const requestBody = { ...body, model }; + + log?.debug?.("SYSTEMONE", `${provider.toUpperCase()} | ${model}`); + + let providerResponse; + try { + providerResponse = await fetch(cfg.baseUrl, { + method: "POST", + headers, + body: JSON.stringify(requestBody), + ...(typeof AbortSignal?.timeout === "function" + ? { signal: AbortSignal.timeout(FETCH_CONNECT_TIMEOUT_MS) } + : {}), + }); + } catch (error) { + const errMsg = formatProviderError(error, provider, model, HTTP_STATUS.BAD_GATEWAY); + log?.debug?.("SYSTEMONE", `Fetch error: ${errMsg}`); + return createErrorResult(HTTP_STATUS.BAD_GATEWAY, errMsg); + } + + if (!providerResponse.ok) { + const { statusCode, message } = await parseUpstreamError(providerResponse); + const errMsg = formatProviderError(new Error(message), provider, model, statusCode); + log?.debug?.("SYSTEMONE", `Provider error: ${errMsg}`); + return createErrorResult(statusCode, errMsg); + } + + let responseBody; + try { + responseBody = await providerResponse.json(); + } catch { + return createErrorResult(HTTP_STATUS.BAD_GATEWAY, `Invalid JSON response from ${provider}`); + } + + if (onRequestSuccess) await onRequestSuccess(); + + const usage = responseBody?.usage; + return { + success: true, + usage: usage + ? { prompt_tokens: usage.input_tokens || 0, completion_tokens: usage.output_tokens || 0 } + : null, + response: new Response(JSON.stringify(responseBody), { + headers: { + "Content-Type": "application/json", + "Access-Control-Allow-Origin": "*", + }, + }), + }; +} diff --git a/open-sse/providers/index.js b/open-sse/providers/index.js index ab5123b1..641d2a20 100644 --- a/open-sse/providers/index.js +++ b/open-sse/providers/index.js @@ -23,7 +23,7 @@ function buildTransport(transport, oauth) { const MEDIA_KEYS = new Set([ "serviceKinds", "ttsConfig", "sttConfig", "embeddingConfig", "imageConfig", "imageToTextConfig", "videoConfig", "musicConfig", - "searchViaChat", "searchConfig", "fetchConfig", + "searchViaChat", "searchConfig", "fetchConfig", "systemoneConfig", "modelsFetcher", "mediaPriority", "hiddenKinds", ]); diff --git a/open-sse/providers/registry/opencode-zen.js b/open-sse/providers/registry/opencode-zen.js index 9bba1df6..a5932e95 100644 --- a/open-sse/providers/registry/opencode-zen.js +++ b/open-sse/providers/registry/opencode-zen.js @@ -106,7 +106,6 @@ export default { { id: "union-alpha", name: "Union Alpha", supportedFormats: ["claude"] }, // Free tier on the keyed lane (chat completions) { id: "deepseek-v4-flash-free", name: "DeepSeek V4 Flash Free", supportedFormats: ["openai"] }, - { id: "mimo-v2.6-flash-free", name: "MiMo V2.6 Flash Free", supportedFormats: ["openai"] }, { id: "mimo-v2.5-free", name: "MiMo V2.5 Free", supportedFormats: ["openai"] }, { id: "ling-3.0-flash-fin-free", name: "Ling 3.0 Flash Fin Free", supportedFormats: ["openai"] }, { id: "nemotron-3-ultra-free", name: "Nemotron 3 Ultra Free", supportedFormats: ["openai"] }, diff --git a/open-sse/providers/registry/opencode.js b/open-sse/providers/registry/opencode.js index b0705066..22ba0c73 100644 --- a/open-sse/providers/registry/opencode.js +++ b/open-sse/providers/registry/opencode.js @@ -28,7 +28,16 @@ export default { { id: "muse-spark-1.2-contributor-free", name: "Muse Spark 1.2 Contributor Free", targetFormat: "openai-responses" }, { id: "muse-spark-1.3-contributor-free", name: "Muse Spark 1.3 Contributor Free", targetFormat: "openai-responses" }, { id: "union-alpha", name: "Union Alpha Free", targetFormat: "claude" }, + { id: "jev-1.13-free", name: "Jev 1.13 Free", kind: "decision" }, ], + serviceKinds: ["llm", "systemone"], + systemoneConfig: { + baseUrl: "https://opencode.ai/zen/v1/systemone", + headers: { + "x-opencode-client": "desktop", + "User-Agent": "opencode/1.18.31", + }, + }, modelsFetcher: { url: "https://opencode.ai/zen/v1/models", type: "opencode-free" }, passthroughModels: true, }; diff --git a/src/app/api/v1/systemone/route.js b/src/app/api/v1/systemone/route.js new file mode 100644 index 00000000..0766cf49 --- /dev/null +++ b/src/app/api/v1/systemone/route.js @@ -0,0 +1,21 @@ +import { handleSystemone } from "@/sse/handlers/systemone.js"; + +/** + * Handle CORS preflight + */ +export async function OPTIONS() { + return new Response(null, { + headers: { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "POST, OPTIONS", + "Access-Control-Allow-Headers": "*" + } + }); +} + +/** + * POST /v1/systemone - System One (Jev) decision endpoint + */ +export async function POST(request) { + return await handleSystemone(request); +} diff --git a/src/shared/constants/providers.js b/src/shared/constants/providers.js index 618e3b2f..005d7b45 100644 --- a/src/shared/constants/providers.js +++ b/src/shared/constants/providers.js @@ -5,7 +5,7 @@ import { RISK_NOTICE } from "@/shared/constants/providersDisplay"; const MEDIA_ENTRY_KEYS = [ "serviceKinds", "ttsConfig", "sttConfig", "embeddingConfig", "imageConfig", "imageToTextConfig", "videoConfig", "musicConfig", - "searchViaChat", "searchConfig", "fetchConfig", "credentialFallback", + "searchViaChat", "searchConfig", "fetchConfig", "credentialFallback", "systemoneConfig", "modelsFetcher", "mediaPriority", "hiddenKinds", ]; @@ -79,6 +79,7 @@ export const MEDIA_PROVIDER_KINDS = [ { id: "webFetch", label: "Web Fetch", icon: "language", endpoint: { method: "POST", path: "/v1/web/fetch" } }, { id: "video", label: "Video", icon: "movie", endpoint: { method: "POST", path: "/v1/videos/generations" } }, { id: "music", label: "Music", icon: "music_note", endpoint: { method: "POST", path: "/v1/audio/music" } }, + { id: "systemone", label: "System One", icon: "psychology", endpoint: { method: "POST", path: "/v1/systemone" } }, ]; export const OPENAI_COMPATIBLE_PREFIX = "openai-compatible-"; diff --git a/src/sse/handlers/systemone.js b/src/sse/handlers/systemone.js new file mode 100644 index 00000000..19f7ba08 --- /dev/null +++ b/src/sse/handlers/systemone.js @@ -0,0 +1,152 @@ +import { + getProviderCredentials, + markAccountUnavailable, + clearAccountError, + extractApiKey, + isValidApiKey, +} from "../services/auth.js"; +import { getSettings } from "@/lib/localDb"; +import { getModelInfo } from "../services/model.js"; +import { handleSystemoneCore } from "open-sse/handlers/systemoneCore.js"; +import { errorResponse, unavailableResponse } from "open-sse/utils/error.js"; +import { HTTP_STATUS } from "open-sse/config/runtimeConfig.js"; +import * as log from "../utils/logger.js"; +import { checkAndRefreshToken } from "../services/tokenRefresh.js"; +import { saveRequestUsage } from "@/lib/usageDb.js"; + +/** + * Handle System One (Jev) decision requests for the Next.js server. + * Follows the same auth + account-fallback pattern as handleEmbeddings. + * + * @param {Request} request + */ +export async function handleSystemone(request) { + let body; + try { + body = await request.json(); + } catch { + log.warn("SYSTEMONE", "Invalid JSON body"); + return errorResponse(HTTP_STATUS.BAD_REQUEST, "Invalid JSON body"); + } + + const url = new URL(request.url); + const modelStr = body.model; + + log.request("POST", `${url.pathname} | ${modelStr}`); + + // Log API key (masked) + const apiKey = extractApiKey(request); + if (apiKey) { + log.debug("AUTH", `API Key: ${log.maskKey(apiKey)}`); + } else { + log.debug("AUTH", "No API key provided (local mode)"); + } + + // Enforce API key if enabled in settings + const settings = await getSettings(); + if (settings.requireApiKey) { + if (!apiKey) { + log.warn("AUTH", "Missing API key (requireApiKey=true)"); + return errorResponse(HTTP_STATUS.UNAUTHORIZED, "Missing API key"); + } + const valid = await isValidApiKey(apiKey); + if (!valid) { + log.warn("AUTH", "Invalid API key (requireApiKey=true)"); + return errorResponse(HTTP_STATUS.UNAUTHORIZED, "Invalid API key"); + } + } + + if (!modelStr) { + log.warn("SYSTEMONE", "Missing model"); + return errorResponse(HTTP_STATUS.BAD_REQUEST, "Missing model"); + } + if (body.state === undefined || body.state === null) { + return errorResponse(HTTP_STATUS.BAD_REQUEST, "Missing required field: state"); + } + if (!body.questions || typeof body.questions !== "object" || Array.isArray(body.questions)) { + return errorResponse(HTTP_STATUS.BAD_REQUEST, "Missing required field: questions"); + } + + const modelInfo = await getModelInfo(modelStr); + if (!modelInfo.provider) { + log.warn("SYSTEMONE", "Invalid model format", { model: modelStr }); + return errorResponse(HTTP_STATUS.BAD_REQUEST, "Invalid model format"); + } + + const { provider, model } = modelInfo; + + if (modelStr !== `${provider}/${model}`) { + log.info("ROUTING", `${modelStr} → ${provider}/${model}`); + } else { + log.info("ROUTING", `Provider: ${provider}, Model: ${model}`); + } + + // Credential + fallback loop (mirrors handleEmbeddings) + const excludeConnectionIds = new Set(); + let lastError = null; + let lastStatus = null; + + while (true) { + const credentials = await getProviderCredentials(provider, excludeConnectionIds, model); + + // All accounts unavailable + if (!credentials || credentials.allRateLimited) { + if (credentials?.allRateLimited) { + const errorMsg = lastError || credentials.lastError || "Unavailable"; + const status = lastStatus || Number(credentials.lastErrorCode) || HTTP_STATUS.SERVICE_UNAVAILABLE; + log.warn("SYSTEMONE", `[${provider}/${model}] ${errorMsg} (${credentials.retryAfterHuman})`); + return unavailableResponse(status, `[${provider}/${model}] ${errorMsg}`, credentials.retryAfter, credentials.retryAfterHuman); + } + if (excludeConnectionIds.size === 0) { + log.error("AUTH", `No credentials for provider: ${provider}`); + return errorResponse(HTTP_STATUS.BAD_REQUEST, `No credentials for provider: ${provider}`); + } + log.warn("SYSTEMONE", "No more accounts available", { provider }); + return errorResponse(lastStatus || HTTP_STATUS.SERVICE_UNAVAILABLE, lastError || "All accounts unavailable"); + } + + log.info("AUTH", `\x1b[32mUsing ${provider} account: ${credentials.connectionName}\x1b[0m`); + + const refreshedCredentials = await checkAndRefreshToken(provider, credentials); + + const result = await handleSystemoneCore({ + body, + modelInfo: { provider, model }, + credentials: refreshedCredentials, + log, + onRequestSuccess: async () => { + await clearAccountError(credentials.connectionId, credentials, model); + } + }); + + if (result.success) { + if (result.usage) { + saveRequestUsage({ + provider, + model, + connectionId: credentials.connectionId, + apiKey, + endpoint: url.pathname, + tokens: { + ...result.usage, + total_tokens: result.usage.prompt_tokens + result.usage.completion_tokens, + }, + status: "success", + }).catch(() => {}); + } + return result.response; + } + + const { shouldFallback } = await markAccountUnavailable(credentials.connectionId, result.status, result.error, provider, model); + + if (shouldFallback) { + log.warn("AUTH", `Account ${credentials.connectionName} unavailable (${result.status}), trying fallback`); + excludeConnectionIds.add(credentials.connectionId); + lastError = result.error; + lastStatus = result.status; + continue; + } + + return result.response; + } +}