@juspay/neurolink
Version:
The pipe layer of an AI nervous system: one interface connecting provider neurons to your application, across three inference types — generate, stream and decide. `decide` returns typed, calibrated judgements from a non-generative model (~400ms, ~$0.00002
163 lines • 6.09 kB
JavaScript
/**
* Qwen Code client configurator.
*
* Qwen Code is OpenAI-compatible: its `cli.js` reads `OPENAI_BASE_URL` and
* `OPENAI_API_KEY`, and its settings file carries the same pair under
* `security.auth`. It therefore needs no new proxy route — it points at the
* existing `/v1/chat/completions` door.
*
* Settings shape verified against a real `~/.qwen/settings.json` (`$version` 2)
* from `@qwen-code/qwen-code@0.17.0`:
*
* {
* "security": { "auth": {
* "selectedType": "openai", "apiKey": "...", "baseUrl": "https://..."
* } },
* "model": { "name": "..." },
* "$version": 2
* }
*/
import { homedir } from "os";
import { join } from "path";
import { logger } from "../../utils/logger.js";
import { cloneForSnapshot, isProxyOwnedValue, shouldCaptureSnapshot, writeFileAtomic, } from "./snapshot.js";
/**
* Resolved per call rather than at module load so `detect()` and `apply()`
* agree when HOME changes — under test, and on the `--dev` isolation path.
*/
function getQwenConfigDir() {
return join(homedir(), ".qwen");
}
function getQwenSettingsPath() {
return join(getQwenConfigDir(), "settings.json");
}
/**
* Key under which the user's pre-existing `security.auth` block is stashed,
* inside the settings file itself. Persisting it there (rather than in memory)
* means a restore still works after a crash, or from a different process —
* the same approach the Claude and OpenCode writers take.
*/
const QWEN_ORIGINAL_KEY = "__proxy_original_qwen_auth";
/**
* The auth block this writer last wrote. Lets apply() tell its own block from
* one the user substituted while the proxy was not running — which for Qwen
* carries a real API key.
*/
const QWEN_WRITTEN_KEY = "__proxy_written_qwen_auth";
function readQwenSettings(fs) {
try {
const parsed = JSON.parse(fs.readFileSync(getQwenSettingsPath(), "utf8"));
return parsed !== null && typeof parsed === "object"
? parsed
: null;
}
catch {
return null;
}
}
export async function setQwenProxySettings(baseUrl, proxyKey) {
const fs = await import("fs");
try {
fs.accessSync(getQwenConfigDir());
}
catch {
// Qwen Code not installed — report the skip so no caller prints a success
// message for work that did not happen.
return false;
}
const settings = readQwenSettings(fs) ?? {};
const security = (settings.security ?? {});
const auth = (security.auth ?? {});
// A repeat apply() must not overwrite the snapshot with our own block, which
// would lose the user's real config on the next restore — but a block the
// user wrote while the proxy was gone must replace it. See
// shouldCaptureSnapshot.
const currentAuth = "auth" in security ? security.auth : undefined;
if (shouldCaptureSnapshot({
hasSnapshot: QWEN_ORIGINAL_KEY in settings,
written: settings[QWEN_WRITTEN_KEY],
current: currentAuth,
})) {
settings[QWEN_ORIGINAL_KEY] =
currentAuth === undefined ? null : cloneForSnapshot(currentAuth);
}
auth.selectedType = "openai";
auth.baseUrl = baseUrl;
auth.apiKey = proxyKey || "neurolink-proxy";
security.auth = auth;
settings.security = security;
settings[QWEN_WRITTEN_KEY] = cloneForSnapshot(auth);
await writeFileAtomic(getQwenSettingsPath(), JSON.stringify(settings, null, 2));
return true;
}
export async function clearQwenProxySettings(expectedBaseUrl) {
const fs = await import("fs");
const settings = readQwenSettings(fs);
if (!settings) {
return false;
}
const security = settings.security;
const auth = security?.auth;
if (!security || !auth) {
return false;
}
// The user may have pointed Qwen somewhere else since the proxy started.
// Never clobber a base URL we did not write.
if (expectedBaseUrl &&
typeof auth.baseUrl === "string" &&
auth.baseUrl !== expectedBaseUrl) {
return false;
}
if (!(QWEN_ORIGINAL_KEY in settings)) {
// No snapshot means we cannot prove this block is ours. Leaving a stale
// proxy URL behind is recoverable; destroying a real credential is not.
logger.debug("[proxy] Qwen clear: no original-auth snapshot found, leaving security.auth intact");
return false;
}
// Only restore what we can prove is ours. The base-URL check above lets
// through a block still pointing at the proxy whose key the user rotated
// beside it — overwriting that would destroy a live credential. Our
// bookkeeping keys go either way, so a stale sentinel cannot outlive us.
if (isProxyOwnedValue({ written: settings[QWEN_WRITTEN_KEY], current: auth })) {
const snapshot = settings[QWEN_ORIGINAL_KEY];
if (snapshot === null) {
delete security.auth;
}
else {
security.auth = snapshot;
}
}
else {
logger.debug("[proxy] Qwen clear: security.auth was edited after the proxy wrote it, leaving it intact");
}
delete settings[QWEN_ORIGINAL_KEY];
delete settings[QWEN_WRITTEN_KEY];
settings.security = security;
await writeFileAtomic(getQwenSettingsPath(), JSON.stringify(settings, null, 2));
return true;
}
export const qwenCodeConfigurator = {
id: "qwen-code",
displayName: "Qwen Code",
detect: async () => {
const fs = await import("fs");
try {
fs.accessSync(getQwenConfigDir());
return true;
}
catch {
return false;
}
},
// Qwen speaks OpenAI Chat Completions, so it points at the /v1 door rather
// than the proxy root.
apply: (proxyBaseUrl) => setQwenProxySettings(`${proxyBaseUrl}/v1`),
restore: (proxyBaseUrl) => clearQwenProxySettings(`${proxyBaseUrl}/v1`),
};
export const __qwenCodeTestHooks = {
getQwenConfigDir,
getQwenSettingsPath,
setQwenProxySettings,
clearQwenProxySettings,
};
//# sourceMappingURL=qwenCode.js.map