refactor(open-sse): registry consolidation + DRY media/oauth/adhoc cleanup

- Single-source registry: oauth clientId/tokenUrl, usage URLs, image/embed
  configs, search defaultModel, codex fixedPort, google token url derive.
- Remove 29 unused OmniRoute providers (registry 100→71); media intact.
- De-adhoc: codex literals → registry format/oauth flags; reasoningInject,
  image/embed openrouter headers + xai bodyFields config-driven.
- Add REGISTRY_TEMPLATE.js + expand PROVIDER_DEFAULTS/schema JSDoc.
- Baselines updated; PROVIDERS 62 + alias 90 byte-for-byte, golden snapshots.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
decolua
2026-06-14 13:15:48 +07:00
co-authored by Cursor
parent 9105dd0e25
commit bb9e9aa91f
166 changed files with 3089 additions and 3067 deletions
+64
View File
@@ -0,0 +1,64 @@
/**
* Qoder API constants ported from CLIProxyAPIPlus qoder-provider branch.
*
* Endpoint set:
* openapi.qoder.sh - device flow + userinfo + quota usage
* center.qoder.sh - token refresh (best-effort, currently 403 for device tokens)
* api3.qoder.sh - inference (chat) + model list, requires COSY signing
* qoder.com/device - browser landing page for device authorization
*/
export const QODER_OPENAPI_BASE = "https://openapi.qoder.sh";
export const QODER_CENTER_BASE = "https://center.qoder.sh";
export const QODER_CHAT_BASE = "https://api3.qoder.sh";
export const QODER_LOGIN_URL = "https://qoder.com/device/selectAccounts";
// Device flow endpoints
export const QODER_DEVICE_TOKEN_URL = `${QODER_OPENAPI_BASE}/api/v1/deviceToken/poll`;
export const QODER_USERINFO_URL = `${QODER_OPENAPI_BASE}/api/v1/userinfo`;
export const QODER_QUOTA_USAGE_URL = `${QODER_OPENAPI_BASE}/api/v2/quota/usage`;
export const QODER_REFRESH_TOKEN_URL = `${QODER_CENTER_BASE}/algo/api/v3/user/refresh_token`;
// Inference endpoints (under /algo on api3.qoder.sh, all COSY-signed)
export const QODER_CHAT_SIG_PATH = "/api/v2/service/pro/sse/agent_chat_generation";
export const QODER_CHAT_URL = `${QODER_CHAT_BASE}/algo${QODER_CHAT_SIG_PATH}?FetchKeys=llm_model_result&AgentId=agent_common`;
export const QODER_CHAT_URL_ENCODED = `${QODER_CHAT_URL}&Encode=1`;
export const QODER_MODEL_LIST_URL = `${QODER_CHAT_BASE}/algo/api/v2/model/list`;
// COSY header constants. These are not arbitrary — the upstream signature
// validation matches them against the values used at signing time.
export const QODER_IDE_VERSION = "1.0.0";
export const QODER_CLIENT_TYPE = "5";
export const QODER_DATA_POLICY = "disagree";
export const QODER_LOGIN_VERSION = "v2";
export const QODER_MACHINE_OS = "x86_64_windows";
export const QODER_MACHINE_TYPE = "5";
// Canonical model identifiers. Identity map — keep as a map so callers can
// cheaply test "is this a known qoder model?" before sending the request.
export const QODER_MODEL_MAP = {
// Tier models
auto: "auto",
ultimate: "ultimate",
performance: "performance",
efficient: "efficient",
lite: "lite",
// Frontier models
qmodel: "qmodel",
qmodel_latest: "qmodel_latest",
dmodel: "dmodel",
dfmodel: "dfmodel",
gm51model: "gm51model",
kmodel: "kmodel",
mmodel: "mmodel",
};
// RSA public key for COSY encryption (extracted from Qoder IDE v0.9).
// Matches the CLIProxyAPIPlus branch and live qodercli traffic.
export const QODER_RSA_PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDA8iMH5c02LilrsERw9t6Pv5Nc
4k6Pz1EaDicBMpdpxKduSZu5OANqUq8er4GM95omAGIOPOh+Nx0spthYA2BqGz+l
6HRkPJ7S236FZz73In/KVuLnwI8JJ2CbuJap8kvheCCZpmAWpb/cPx/3Vr/J6I17
XcW+ML9FoCI6AOvOzwIDAQAB
-----END PUBLIC KEY-----`;
+175
View File
@@ -0,0 +1,175 @@
/**
* Qoder COSY (hybrid RSA+AES+MD5) signing, ported from CLIProxyAPIPlus
* qoder-provider branch (internal/auth/qoder/cosy.go).
*
* Every signed request carries:
* - an AES-128-CBC payload of the user info, the AES key wrapped in RSA
* - an MD5 signature over `payload || cosyKey || timestamp || body || sigPath`
* - the body's MD5 hash + length so the server can validate integrity
* - 17 Cosy-* / X-* headers fingerprinting the client (machine id, IDE
* version, organization id, etc.)
*
* The on-the-wire header keys use the same casing as qodercli:
* Cosy-Machineid, not Cosy-MachineID.
*/
import crypto from "crypto";
import { v4 as uuidv4 } from "uuid";
import {
QODER_CLIENT_TYPE,
QODER_DATA_POLICY,
QODER_IDE_VERSION,
QODER_LOGIN_VERSION,
QODER_MACHINE_OS,
QODER_MACHINE_TYPE,
QODER_RSA_PUBLIC_KEY,
} from "./constants.js";
// AES-128 wants a 16-byte key. Match qodercli/Veria: take the first 16 chars
// of a fresh UUID's canonical string (hyphens included). The key is fresh
// per request so even though the IV reuses the key bytes, each request still
// has a unique IV.
function generateAesKey() {
return uuidv4().slice(0, 16);
}
function pkcs7Pad(data, blockSize) {
const padding = blockSize - (data.length % blockSize);
const padded = Buffer.alloc(data.length + padding, padding);
data.copy(padded, 0);
return padded;
}
function aesEncryptCbcBase64(plaintext, keyStr) {
const keyBytes = Buffer.from(keyStr, "utf8");
if (keyBytes.length !== 16) {
throw new Error(`aes key must be 16 bytes, got ${keyBytes.length}`);
}
const iv = keyBytes.subarray(0, 16);
const cipher = crypto.createCipheriv("aes-128-cbc", keyBytes, iv);
cipher.setAutoPadding(false);
const padded = pkcs7Pad(Buffer.from(plaintext, "utf8"), 16);
const encrypted = Buffer.concat([cipher.update(padded), cipher.final()]);
return encrypted.toString("base64");
}
function rsaEncryptBase64(data) {
const encrypted = crypto.publicEncrypt(
{ key: QODER_RSA_PUBLIC_KEY, padding: crypto.constants.RSA_PKCS1_PADDING },
Buffer.from(data, "utf8"),
);
return encrypted.toString("base64");
}
function encryptUserInfo(userInfo) {
const aesKey = generateAesKey();
const plaintext = JSON.stringify(userInfo);
const infoB64 = aesEncryptCbcBase64(plaintext, aesKey);
const cosyKeyB64 = rsaEncryptBase64(aesKey);
return { cosyKey: cosyKeyB64, info: infoB64 };
}
function md5Hex(input) {
return crypto.createHash("md5").update(input).digest("hex");
}
/**
* Strip the leading "/algo" prefix from the request path. Matches qodercli
* convention. Empty input returns "".
*/
function computeSigPath(requestUrl) {
let pathname;
try {
pathname = new URL(requestUrl).pathname || "";
} catch {
return "";
}
if (pathname.startsWith("/algo")) {
return pathname.slice("/algo".length);
}
return pathname;
}
/**
* Generate a fresh machine UUID. Persisted on the connection record so
* every request from the same auth carries the same machineId.
*/
export function generateMachineId() {
return uuidv4();
}
/**
* Build the full Cosy-* header set for a single Qoder request.
*
* @param {Buffer|Uint8Array|string} body The exact bytes that will be sent.
* For GET requests pass an empty Buffer / "".
* @param {string} requestUrl Full request URL (used for sigPath).
* @param {object} creds
* @param {string} creds.userId Stable Qoder user id.
* @param {string} creds.authToken Device access token (`dt-...`).
* @param {string} [creds.name] Display name (optional).
* @param {string} [creds.email] Email (optional, can be empty).
* @param {string} [creds.machineId] Persisted machine UUID.
* @returns {Record<string, string>} Header map ready to merge onto fetch().
*/
export function buildCosyHeaders(body, requestUrl, creds) {
if (!creds?.userId) throw new Error("cosy: user id is empty");
if (!creds?.authToken) throw new Error("cosy: auth token is empty");
const bodyBuf = Buffer.isBuffer(body)
? body
: typeof body === "string"
? Buffer.from(body, "latin1")
: Buffer.from(body || []);
const { cosyKey, info } = encryptUserInfo({
uid: creds.userId,
security_oauth_token: creds.authToken,
name: creds.name || "",
aid: "",
email: creds.email || "",
});
const timestamp = String(Math.floor(Date.now() / 1000));
const requestId = uuidv4();
const payloadJson = JSON.stringify({
version: "v1",
requestId,
info,
cosyVersion: QODER_IDE_VERSION,
ideVersion: "",
});
const payloadB64 = Buffer.from(payloadJson, "utf8").toString("base64");
const sigPath = computeSigPath(requestUrl);
const sigInput = `${payloadB64}\n${cosyKey}\n${timestamp}\n${bodyBuf.toString("latin1")}\n${sigPath}`;
const sig = md5Hex(Buffer.from(sigInput, "latin1"));
const machineId = creds.machineId || generateMachineId();
const bodyHash = md5Hex(bodyBuf);
const bodyLength = String(bodyBuf.length);
return {
Authorization: `Bearer COSY.${payloadB64}.${sig}`,
"Cosy-Key": cosyKey,
"Cosy-User": creds.userId,
"Cosy-Date": timestamp,
"Cosy-Version": QODER_IDE_VERSION,
"Cosy-Machineid": machineId,
"Cosy-Machinetoken": machineId,
"Cosy-Machinetype": QODER_MACHINE_TYPE,
"Cosy-Machineos": QODER_MACHINE_OS,
"Cosy-Clienttype": QODER_CLIENT_TYPE,
"Cosy-Clientip": "127.0.0.1",
"Cosy-Bodyhash": bodyHash,
"Cosy-Bodylength": bodyLength,
"Cosy-Sigpath": sigPath,
"Cosy-Data-Policy": QODER_DATA_POLICY,
"Cosy-Organization-Id": "",
"Cosy-Organization-Tags": "",
"Login-Version": QODER_LOGIN_VERSION,
"X-Request-Id": uuidv4(),
};
}
+55
View File
@@ -0,0 +1,55 @@
/**
* Qoder body encoding ported from qoder2api's QoderEncoding.java (via the
* CLIProxyAPIPlus qoder-provider branch).
*
* Algorithm:
* 1. base64-encode the plaintext bytes (standard alphabet).
* 2. Rearrange: split into thirds, reorder as [tail][mid][head].
* 3. Substitute each character via a custom alphabet mapping.
*
* The encoded body must be sent with `&Encode=1` appended to the URL so the
* server decodes in reverse. The obfuscation prevents Alibaba Cloud WAF from
* pattern-matching the plaintext request body.
*/
const QODER_STD_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
const QODER_CUSTOM_ALPHABET = "_doRTgHZBKcGVjlvpC,@aFSx#DPuNJme&i*MzLOEn)sUrthbf%Y^w.(kIQyXqWA!";
const QODER_S2C = (() => {
const table = new Int16Array(128).fill(-1);
for (let i = 0; i < 64; i++) {
table[QODER_STD_ALPHABET.charCodeAt(i)] = QODER_CUSTOM_ALPHABET.charCodeAt(i);
}
table["=".charCodeAt(0)] = "$".charCodeAt(0);
return table;
})();
/**
* Encode plaintext bytes/string using Qoder's WAF-bypass scheme.
* @param {Buffer|Uint8Array|string} plaintext
* @returns {string} encoded string
*/
export function qoderEncodeBody(plaintext) {
const buf = Buffer.isBuffer(plaintext)
? plaintext
: typeof plaintext === "string"
? Buffer.from(plaintext, "utf8")
: Buffer.from(plaintext);
const std = buf.toString("base64");
const n = std.length;
const a = Math.floor(n / 3);
// [tail][mid][head]
const rearranged = std.slice(n - a) + std.slice(a, n - a) + std.slice(0, a);
const out = Buffer.alloc(n);
for (let i = 0; i < n; i++) {
const c = rearranged.charCodeAt(i);
if (c < 128 && QODER_S2C[c] >= 0) {
out[i] = QODER_S2C[c];
} else {
out[i] = c;
}
}
return out.toString("latin1");
}