mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-01 04:42:10 +03:00
* refactor(cli): remove legacy Qwen Code integration * refactor(qwen): remove deprecated Qwen OAuth provider * feat(cli): rebuild Qwen Code integration for upstream V4 * fix(qwen): clear stale CLI auth on reset * test(qwen): align retired provider coverage * fix(db): renumber qwen-cleanup migration 129 -> 130 release/v3.8.49 tip took slot 129 via #7843 (usage_history_codex_strong_identity, itself renumbered from 128 during the #7838/#7840 base-red cleanup) after this branch forked; renumber remove_unregistered_qwen_data to 130. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com>
253 lines
8.6 KiB
TypeScript
253 lines
8.6 KiB
TypeScript
type JsonRecord = Record<string, unknown>;
|
|
|
|
export const QWEN_CODE_ENV_KEY = "OMNIROUTE_API_KEY";
|
|
|
|
const LEGACY_ENV_KEYS = new Set(["OPENAI_API_KEY", "ANTHROPIC_API_KEY", "GEMINI_API_KEY"]);
|
|
|
|
const isRecord = (value: unknown): value is JsonRecord =>
|
|
typeof value === "object" && value !== null && !Array.isArray(value);
|
|
|
|
const cloneRecord = (value: unknown): JsonRecord => (isRecord(value) ? { ...value } : {});
|
|
|
|
export const normalizeQwenCodeBaseUrl = (value: unknown): string => {
|
|
const baseUrl = String(value || "")
|
|
.trim()
|
|
.replace(/\/+$/, "");
|
|
if (!baseUrl) return "";
|
|
return baseUrl.endsWith("/v1") ? baseUrl : `${baseUrl}/v1`;
|
|
};
|
|
|
|
/**
|
|
* Identifies only entries owned by OmniRoute. Generic custom endpoints are not
|
|
* considered managed: users can keep any other OpenAI-compatible provider.
|
|
*/
|
|
export const isManagedQwenCodeModel = (value: unknown): boolean => {
|
|
if (!isRecord(value)) return false;
|
|
if (value.envKey === QWEN_CODE_ENV_KEY || value.id === "omniroute") return true;
|
|
|
|
return (
|
|
typeof value.name === "string" &&
|
|
value.name.endsWith(" (OmniRoute)") &&
|
|
typeof value.envKey === "string" &&
|
|
LEGACY_ENV_KEYS.has(value.envKey)
|
|
);
|
|
};
|
|
|
|
const unwrapProviderModels = (value: unknown): unknown => {
|
|
if (Array.isArray(value)) return [...value];
|
|
if (isRecord(value) && Array.isArray(value.models)) return [...value.models];
|
|
return value;
|
|
};
|
|
|
|
const getProviderMap = (value: unknown): JsonRecord => {
|
|
if (Array.isArray(value)) {
|
|
const migrated: Record<string, unknown[]> = {};
|
|
for (const entry of value) {
|
|
if (!isRecord(entry) || typeof entry.id !== "string") continue;
|
|
const providerId = typeof entry.authType === "string" ? entry.authType : "openai";
|
|
const model = { ...entry };
|
|
delete model.authType;
|
|
(migrated[providerId] ||= []).push(model);
|
|
}
|
|
return migrated;
|
|
}
|
|
if (!isRecord(value)) return {};
|
|
return Object.fromEntries(
|
|
Object.entries(value).map(([providerId, models]) => [providerId, unwrapProviderModels(models)])
|
|
);
|
|
};
|
|
|
|
const filterManagedModels = (value: unknown): { value: unknown; removed: JsonRecord[] } => {
|
|
if (!Array.isArray(value)) return { value, removed: [] };
|
|
|
|
const removed = value.filter(isManagedQwenCodeModel).filter(isRecord);
|
|
return {
|
|
value: value.filter((entry) => !isManagedQwenCodeModel(entry)),
|
|
removed,
|
|
};
|
|
};
|
|
|
|
const removeEmptyObject = (parent: JsonRecord, key: string): void => {
|
|
if (isRecord(parent[key]) && Object.keys(parent[key]).length === 0) delete parent[key];
|
|
};
|
|
|
|
export type QwenCodeConfigInput = {
|
|
baseUrl: string;
|
|
model: string;
|
|
modelName?: string;
|
|
};
|
|
|
|
export const buildQwenCodeModel = ({
|
|
baseUrl,
|
|
model,
|
|
modelName,
|
|
}: QwenCodeConfigInput): JsonRecord => {
|
|
const normalizedBaseUrl = normalizeQwenCodeBaseUrl(baseUrl);
|
|
const normalizedModel = String(model || "").trim();
|
|
if (!normalizedBaseUrl || !normalizedModel) {
|
|
throw new Error("Qwen Code requires a base URL and model");
|
|
}
|
|
|
|
return {
|
|
id: normalizedModel,
|
|
name: `${String(modelName || normalizedModel).trim()} (OmniRoute)`,
|
|
envKey: QWEN_CODE_ENV_KEY,
|
|
baseUrl: normalizedBaseUrl,
|
|
};
|
|
};
|
|
|
|
/**
|
|
* Merge one OmniRoute model into Qwen Code's current V4 settings contract.
|
|
* `modelProviders.openai` is a bare ModelConfig[] array. Other settings and
|
|
* user-owned provider entries are preserved.
|
|
*/
|
|
export const mergeQwenCodeSettings = (
|
|
existing: unknown,
|
|
input: QwenCodeConfigInput
|
|
): JsonRecord => {
|
|
const next = cloneRecord(existing);
|
|
const modelEntry = buildQwenCodeModel(input);
|
|
const modelProviders = getProviderMap(next.modelProviders);
|
|
|
|
for (const [providerId, value] of Object.entries(modelProviders)) {
|
|
const filtered = filterManagedModels(value);
|
|
modelProviders[providerId] = filtered.value;
|
|
if (Array.isArray(filtered.value) && filtered.value.length === 0 && providerId !== "openai") {
|
|
delete modelProviders[providerId];
|
|
}
|
|
}
|
|
|
|
const openaiModels = Array.isArray(modelProviders.openai) ? modelProviders.openai : [];
|
|
modelProviders.openai = [...openaiModels, modelEntry];
|
|
next.modelProviders = modelProviders;
|
|
|
|
const security = cloneRecord(next.security);
|
|
const auth = cloneRecord(security.auth);
|
|
const previousAuthBaseUrl = normalizeQwenCodeBaseUrl(auth.baseUrl);
|
|
const replacingLegacyAuth =
|
|
auth.selectedType === "openai" && previousAuthBaseUrl === modelEntry.baseUrl;
|
|
|
|
auth.selectedType = "openai";
|
|
if (replacingLegacyAuth) {
|
|
delete auth.apiKey;
|
|
delete auth.baseUrl;
|
|
}
|
|
security.auth = auth;
|
|
next.security = security;
|
|
|
|
next.model = {
|
|
...cloneRecord(next.model),
|
|
name: modelEntry.id,
|
|
baseUrl: modelEntry.baseUrl,
|
|
};
|
|
|
|
if (next.selectedProvider === "omniroute") delete next.selectedProvider;
|
|
|
|
return next;
|
|
};
|
|
|
|
export const findOmniRouteQwenCodeModel = (settings: unknown): JsonRecord | undefined => {
|
|
if (!isRecord(settings)) return undefined;
|
|
const providers = settings.modelProviders;
|
|
if (Array.isArray(providers))
|
|
return providers.find(isManagedQwenCodeModel) as JsonRecord | undefined;
|
|
if (!isRecord(providers)) return undefined;
|
|
|
|
for (const value of Object.values(providers)) {
|
|
const models = unwrapProviderModels(value);
|
|
if (!Array.isArray(models)) continue;
|
|
const managed = models.find(isManagedQwenCodeModel);
|
|
if (isRecord(managed)) return managed;
|
|
}
|
|
return undefined;
|
|
};
|
|
|
|
export const hasOmniRouteQwenCodeConfig = (settings: unknown): boolean =>
|
|
findOmniRouteQwenCodeModel(settings) !== undefined;
|
|
|
|
/** Remove only OmniRoute-owned Qwen Code entries and selection state. */
|
|
export const removeQwenCodeSettings = (existing: unknown): JsonRecord => {
|
|
const next = cloneRecord(existing);
|
|
const originalProviders = next.modelProviders;
|
|
const removed: JsonRecord[] = [];
|
|
|
|
if (Array.isArray(originalProviders)) {
|
|
const filtered = filterManagedModels(originalProviders);
|
|
next.modelProviders = filtered.value;
|
|
removed.push(...filtered.removed);
|
|
if ((filtered.value as unknown[]).length === 0) delete next.modelProviders;
|
|
} else if (isRecord(originalProviders)) {
|
|
const providers = { ...originalProviders };
|
|
for (const [providerId, rawValue] of Object.entries(providers)) {
|
|
const unwrapped = unwrapProviderModels(rawValue);
|
|
const filtered = filterManagedModels(unwrapped);
|
|
removed.push(...filtered.removed);
|
|
if (Array.isArray(filtered.value) && filtered.value.length === 0) {
|
|
delete providers[providerId];
|
|
} else {
|
|
providers[providerId] = filtered.value;
|
|
}
|
|
}
|
|
if (Object.keys(providers).length === 0) delete next.modelProviders;
|
|
else next.modelProviders = providers;
|
|
}
|
|
|
|
const model = cloneRecord(next.model);
|
|
const selectedMatchesRemoved = removed.some(
|
|
(entry) =>
|
|
entry.id === model.name &&
|
|
(!model.baseUrl ||
|
|
normalizeQwenCodeBaseUrl(entry.baseUrl) === normalizeQwenCodeBaseUrl(model.baseUrl))
|
|
);
|
|
|
|
if (selectedMatchesRemoved) {
|
|
delete model.name;
|
|
delete model.baseUrl;
|
|
if (Object.keys(model).length === 0) delete next.model;
|
|
else next.model = model;
|
|
}
|
|
|
|
const security = cloneRecord(next.security);
|
|
const auth = cloneRecord(security.auth);
|
|
const remainingOpenai = isRecord(next.modelProviders) ? next.modelProviders.openai : undefined;
|
|
const authBaseUrl = normalizeQwenCodeBaseUrl(auth.baseUrl);
|
|
const authMatchesRemoved = removed.some(
|
|
(entry) => normalizeQwenCodeBaseUrl(entry.baseUrl) === authBaseUrl
|
|
);
|
|
if (authMatchesRemoved) {
|
|
delete auth.apiKey;
|
|
delete auth.baseUrl;
|
|
if (auth.selectedType === "openai" && !Array.isArray(remainingOpenai)) {
|
|
delete auth.selectedType;
|
|
}
|
|
if (Object.keys(auth).length === 0) delete security.auth;
|
|
else security.auth = auth;
|
|
if (Object.keys(security).length === 0) delete next.security;
|
|
else next.security = security;
|
|
}
|
|
|
|
if (next.selectedProvider === "omniroute") delete next.selectedProvider;
|
|
removeEmptyObject(next, "modelProviders");
|
|
return next;
|
|
};
|
|
|
|
const OWNED_ENV_LINE = /^\s*(?:export\s+)?OMNIROUTE_API_KEY\s*=/;
|
|
|
|
export const mergeQwenCodeEnv = (existing: unknown, apiKey: unknown): string => {
|
|
const lines = String(existing || "")
|
|
.split(/\r?\n/)
|
|
.filter((line) => !OWNED_ENV_LINE.test(line));
|
|
|
|
while (lines.length > 0 && lines.at(-1) === "") lines.pop();
|
|
lines.push(`${QWEN_CODE_ENV_KEY}=${JSON.stringify(String(apiKey || "sk_omniroute"))}`);
|
|
return `${lines.join("\n")}\n`;
|
|
};
|
|
|
|
export const removeQwenCodeEnv = (existing: unknown): string => {
|
|
const lines = String(existing || "")
|
|
.split(/\r?\n/)
|
|
.filter((line) => !OWNED_ENV_LINE.test(line));
|
|
while (lines.length > 0 && lines.at(-1) === "") lines.pop();
|
|
return lines.length > 0 ? `${lines.join("\n")}\n` : "";
|
|
};
|