feat(qoder): port Kiro-style provider integration with COSY signing

Replaces the Qoder placeholder with a real free-tier provider:

- Device-flow OAuth: PKCE + nonce generated locally, user authorizes at
  qoder.com/device/selectAccounts, poll openapi.qoder.sh until token
- COSY signing (RSA-1024 + AES-128-CBC + MD5) for chat / model-list
- WAF-bypass body encoding (custom-alphabet base64 + thirds rearrange)
- Live model_config catalog from /algo/api/v2/model/list, cached 1h
- 11 models registered (auto/ultimate/performance/efficient/lite +
  6 frontier *model ids)
- Usage fetcher for openapi.qoder.sh/api/v2/quota/usage
- Dashboard live-models resolver, provider test, OAuth modal hookup
- 24 unit tests covering encoder, PKCE, COSY headers, sigPath stripping
This commit is contained in:
Simon Shi
2026-05-29 17:36:27 +07:00
committed by decolua
parent 468c61b2ac
commit a6fd84691b
20 changed files with 1506 additions and 132 deletions
@@ -5,6 +5,7 @@ import { GEMINI_CONFIG } from "@/lib/oauth/constants/oauth";
import { refreshGoogleToken, updateProviderCredentials } from "@/sse/services/tokenRefresh";
import { resolveOllamaLocalHost } from "open-sse/config/providers.js";
import { resolveKiroModels } from "open-sse/services/kiroModels.js";
import { resolveQoderModels } from "open-sse/services/qoderModels.js";
const GEMINI_CLI_MODELS_URL = "https://cloudcode-pa.googleapis.com/v1internal:fetchAvailableModels";
@@ -286,6 +287,41 @@ const PROVIDER_MODELS_CONFIG = {
return { models: [], warning };
}
},
qoder: {
customResolver: async (connection) => {
const credentials = {
accessToken: connection.accessToken,
refreshToken: connection.refreshToken,
email: connection.email,
displayName: connection.displayName,
providerSpecificData: connection.providerSpecificData || {},
};
let warning;
try {
const result = await resolveQoderModels(credentials, { forceRefresh: true });
if (result?.models?.length) {
return {
models: result.models.map((m) => ({
// Use the canonical "qoder/<key>" id so the dashboard
// surfaces the same identifier the chat router expects.
id: `qoder/${m.id}`,
name: m.name,
contextLength: m.contextLength,
isVL: m.isVL,
isReasoning: m.isReasoning,
maxOutputTokens: m.maxOutputTokens,
description: m.description,
})),
};
}
warning = "Qoder returned no models; falling back to static catalog.";
} catch (error) {
warning = `Failed to fetch Qoder models: ${error.message}`;
console.log("Failed to fetch Qoder models dynamically, falling back to static:", error.message);
}
return { models: [], warning };
},
},
"gemini-cli": {
customResolver: buildOAuthResolver({
refreshFn: (conn) => refreshGoogleToken(conn.refreshToken, GEMINI_CONFIG.clientId, GEMINI_CONFIG.clientSecret),
@@ -61,6 +61,15 @@ const OAUTH_TEST_CONFIG = {
},
qwen: { checkExpiry: true, refreshable: true },
kiro: { checkExpiry: true, refreshable: true },
qoder: {
// Test by hitting Qoder's userinfo endpoint with the device token.
url: "https://openapi.qoder.sh/api/v1/userinfo",
method: "GET",
authHeader: "Authorization",
authPrefix: "Bearer ",
checkExpiry: true,
refreshable: false,
},
"kimi-coding": { checkExpiry: true, refreshable: false },
cursor: { tokenExists: true },
kilocode: {