opencode-qoder 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,45 @@
1
+ # opencode-qoder
2
+
3
+ Qoder Global provider plugin for [opencode](https://opencode.ai/). This is a port of the global-only pieces of `pi-provider-qoder`: PAT exchange, COSY request signing, Qoder body encoding, chat SSE parsing, reasoning, image input, and tool calls.
4
+
5
+ Qoder China endpoints and model aliases are intentionally not included.
6
+
7
+ ## Build
8
+
9
+ ```bash
10
+ npm install
11
+ npm run build
12
+ ```
13
+
14
+ ## Installation
15
+
16
+ Add the built plugin to `opencode.json` in this repo, or adjust the relative path for your config location:
17
+
18
+ ```json
19
+ {
20
+ "$schema": "https://opencode.ai/config.json",
21
+ "plugin": ["opencode-qoder"],
22
+ "model": "qoder/auto"
23
+ }
24
+ ```
25
+
26
+ The plugin registers provider `qoder` and these models: `auto`, `ultimate`, `performance`, `efficient`, `lite`, `qmodel`, `qmodel_latest`, `dmodel`, `dfmodel`, `gm51model`, `kmodel`, and `mmodel`.
27
+
28
+ ## Authenticate
29
+
30
+ Use a Qoder Personal Access Token (`pt-...`). A PAT is exchanged for a short-lived Qoder job token automatically before requests.
31
+
32
+ ```bash
33
+ export QODER_PERSONAL_ACCESS_TOKEN="pt-..."
34
+ opencode
35
+ ```
36
+
37
+ You can also run opencode's auth flow after the plugin is loaded:
38
+
39
+ ```text
40
+ /connect qoder
41
+ ```
42
+
43
+ Choose `Personal Access Token` and paste the PAT.
44
+
45
+ After adding or changing the plugin config, quit and restart opencode. Plugins and provider config are loaded at startup.
package/dist/auth.d.ts ADDED
@@ -0,0 +1,38 @@
1
+ export interface QoderCredentials {
2
+ access: string;
3
+ refresh: string;
4
+ expires: number;
5
+ userID: string;
6
+ email: string;
7
+ name: string;
8
+ machineID: string;
9
+ }
10
+ export interface QoderProviderOptions {
11
+ apiKey?: string;
12
+ personalAccessToken?: string;
13
+ qoderUserID?: string;
14
+ qoderEmail?: string;
15
+ qoderName?: string;
16
+ qoderMachineID?: string;
17
+ }
18
+ export declare const PAT_REFRESH_PREFIX = "pat";
19
+ export declare function encodePatRefresh(pat: string, jobRefreshToken: string, userID: string, machineID: string): string;
20
+ export declare function decodePatRefresh(refresh: string): {
21
+ pat: string;
22
+ jobRefreshToken: string;
23
+ userID: string;
24
+ machineID: string;
25
+ };
26
+ export declare function encodeOAuthRefresh(refreshToken: string, userID: string, machineID: string): string;
27
+ export declare function decodeOAuthRefresh(refresh: string): {
28
+ refreshToken: string;
29
+ userID: string;
30
+ machineID: string;
31
+ };
32
+ export declare function credentialsFromPat(pat: string): Promise<QoderCredentials>;
33
+ export declare function refreshOAuthCredential(credential: QoderCredentials): Promise<QoderCredentials>;
34
+ export declare function resolveQoderCredentials(options?: QoderProviderOptions): Promise<QoderCredentials>;
35
+ export declare function generatePKCE(): {
36
+ codeVerifier: string;
37
+ codeChallenge: string;
38
+ };
package/dist/auth.js ADDED
@@ -0,0 +1,175 @@
1
+ import crypto from "node:crypto";
2
+ import { getMachineId } from "./cosy.js";
3
+ import { QODER_EXCHANGE_URL, QODER_PAT_ENV, QODER_REFRESH_URL, QODER_USERINFO_URL, USER_AGENT } from "./constants.js";
4
+ export const PAT_REFRESH_PREFIX = "pat";
5
+ const credentialsCache = new Map();
6
+ export function encodePatRefresh(pat, jobRefreshToken, userID, machineID) {
7
+ return [PAT_REFRESH_PREFIX, pat, jobRefreshToken, userID, machineID].join("|");
8
+ }
9
+ export function decodePatRefresh(refresh) {
10
+ const parts = refresh.split("|");
11
+ return {
12
+ pat: parts[1] || "",
13
+ jobRefreshToken: parts[2] || "",
14
+ userID: parts[3] || "",
15
+ machineID: parts[4] || "",
16
+ };
17
+ }
18
+ export function encodeOAuthRefresh(refreshToken, userID, machineID) {
19
+ return [refreshToken, userID, machineID].join("|");
20
+ }
21
+ export function decodeOAuthRefresh(refresh) {
22
+ const parts = refresh.split("|");
23
+ return {
24
+ refreshToken: parts[0] || "",
25
+ userID: parts[1] || "",
26
+ machineID: parts[2] || "",
27
+ };
28
+ }
29
+ function getEnvPat() {
30
+ for (const key of QODER_PAT_ENV) {
31
+ const value = process.env[key];
32
+ if (value)
33
+ return value;
34
+ }
35
+ return "";
36
+ }
37
+ function parseExpiresAt(expiresAt, expiresIn) {
38
+ if (expiresAt) {
39
+ const parsed = Date.parse(expiresAt);
40
+ if (!Number.isNaN(parsed))
41
+ return parsed;
42
+ const numeric = Number.parseInt(expiresAt, 10);
43
+ if (Number.isFinite(numeric) && numeric > 0)
44
+ return numeric;
45
+ }
46
+ if (expiresIn && expiresIn > 0) {
47
+ // PAT exchange returns milliseconds; browser device flow returns seconds.
48
+ return Date.now() + (expiresIn > 7 * 24 * 60 * 60 ? expiresIn : expiresIn * 1000);
49
+ }
50
+ return Date.now() + 24 * 60 * 60 * 1000;
51
+ }
52
+ async function fetchUserInfo(jobToken) {
53
+ try {
54
+ const res = await fetch(QODER_USERINFO_URL, {
55
+ headers: {
56
+ Authorization: `Bearer ${jobToken}`,
57
+ Accept: "application/json",
58
+ "User-Agent": USER_AGENT,
59
+ "Cosy-Version": "1.0.1",
60
+ "Cosy-ClientType": "5",
61
+ },
62
+ });
63
+ if (!res.ok)
64
+ return { userID: "", email: "", name: "" };
65
+ const info = (await res.json());
66
+ return {
67
+ userID: info.id || "",
68
+ email: info.email || "",
69
+ name: info.name || info.username || "",
70
+ };
71
+ }
72
+ catch {
73
+ return { userID: "", email: "", name: "" };
74
+ }
75
+ }
76
+ export async function credentialsFromPat(pat) {
77
+ const cached = credentialsCache.get(pat);
78
+ if (cached) {
79
+ const resolved = await cached;
80
+ if (resolved.expires > Date.now())
81
+ return resolved;
82
+ credentialsCache.delete(pat);
83
+ }
84
+ const pending = (async () => {
85
+ const res = await fetch(QODER_EXCHANGE_URL, {
86
+ method: "POST",
87
+ headers: {
88
+ "Content-Type": "application/json",
89
+ Accept: "application/json",
90
+ "User-Agent": USER_AGENT,
91
+ "Cosy-Version": "1.0.1",
92
+ "Cosy-ClientType": "5",
93
+ },
94
+ body: JSON.stringify({ personal_token: pat }),
95
+ });
96
+ if (!res.ok) {
97
+ const text = await res.text().catch(() => "");
98
+ throw new Error(`Qoder PAT exchange failed: ${res.status} ${res.statusText}. ${text.slice(0, 200)}`);
99
+ }
100
+ const data = (await res.json());
101
+ if (!data.token)
102
+ throw new Error("Qoder PAT exchange returned no job token");
103
+ const profile = await fetchUserInfo(data.token);
104
+ const machineID = getMachineId();
105
+ return {
106
+ refresh: encodePatRefresh(pat, data.refresh_token || "", profile.userID, machineID),
107
+ access: data.token,
108
+ expires: parseExpiresAt(data.expires_at, data.expires_in) - 5 * 60 * 1000,
109
+ userID: profile.userID || "qoder-user",
110
+ email: profile.email || "user@qoder.com",
111
+ name: profile.name || "Qoder User",
112
+ machineID,
113
+ };
114
+ })();
115
+ credentialsCache.set(pat, pending);
116
+ try {
117
+ const resolved = await pending;
118
+ credentialsCache.set(pat, resolved);
119
+ return resolved;
120
+ }
121
+ catch (error) {
122
+ credentialsCache.delete(pat);
123
+ throw error;
124
+ }
125
+ }
126
+ export async function refreshOAuthCredential(credential) {
127
+ const { refreshToken, userID, machineID } = decodeOAuthRefresh(credential.refresh);
128
+ if (!refreshToken)
129
+ return credential;
130
+ const response = await fetch(QODER_REFRESH_URL, {
131
+ method: "POST",
132
+ headers: {
133
+ "Content-Type": "application/json",
134
+ Authorization: `Bearer ${credential.access}`,
135
+ Accept: "application/json",
136
+ "User-Agent": USER_AGENT,
137
+ },
138
+ body: JSON.stringify({ refreshToken }),
139
+ });
140
+ if (!response.ok)
141
+ return credential;
142
+ const data = (await response.json());
143
+ if (!data.token)
144
+ return credential;
145
+ return {
146
+ ...credential,
147
+ access: data.token,
148
+ refresh: encodeOAuthRefresh(data.refresh_token || refreshToken, userID || credential.userID, machineID || credential.machineID),
149
+ expires: parseExpiresAt(data.expires_at, data.expires_in) - 5 * 60 * 1000,
150
+ userID: userID || credential.userID,
151
+ machineID: machineID || credential.machineID,
152
+ };
153
+ }
154
+ export async function resolveQoderCredentials(options = {}) {
155
+ const token = options.personalAccessToken || options.apiKey || getEnvPat();
156
+ if (!token) {
157
+ throw new Error("Qoder credentials not set. Run `/connect qoder` in opencode or set QODER_PERSONAL_ACCESS_TOKEN.");
158
+ }
159
+ if (token.startsWith("pt-"))
160
+ return credentialsFromPat(token);
161
+ return {
162
+ access: token,
163
+ refresh: "",
164
+ expires: Date.now() + 60 * 60 * 1000,
165
+ userID: options.qoderUserID || "qoder-user",
166
+ email: options.qoderEmail || "user@qoder.com",
167
+ name: options.qoderName || "Qoder User",
168
+ machineID: options.qoderMachineID || getMachineId(),
169
+ };
170
+ }
171
+ export function generatePKCE() {
172
+ const codeVerifier = crypto.randomBytes(32).toString("base64url");
173
+ const codeChallenge = crypto.createHash("sha256").update(codeVerifier).digest("base64url");
174
+ return { codeVerifier, codeChallenge };
175
+ }
@@ -0,0 +1,30 @@
1
+ export declare const PROVIDER_ID = "qoder";
2
+ export declare const PROVIDER_NAME = "Qoder";
3
+ export declare const QODER_BASE_URL = "https://api3.qoder.sh/";
4
+ export declare const QODER_OPENAPI_URL = "https://openapi.qoder.sh";
5
+ export declare const QODER_CENTER_URL = "https://center.qoder.sh";
6
+ export declare const QODER_MANAGE_URL = "https://qoder.com";
7
+ export declare const QODER_MODEL_LIST_URL = "https://api3.qoder.sh/algo/api/v2/model/list";
8
+ export declare const QODER_CHAT_URL = "https://api3.qoder.sh/algo/api/v2/service/pro/sse/agent_chat_generation?FetchKeys=llm_model_result&AgentId=agent_common&Encode=1";
9
+ export declare const QODER_EXCHANGE_URL = "https://openapi.qoder.sh/api/v1/jobToken/exchange";
10
+ export declare const QODER_USERINFO_URL = "https://openapi.qoder.sh/api/v1/userinfo";
11
+ export declare const QODER_REFRESH_URL = "https://center.qoder.sh/algo/api/v3/user/refresh_token";
12
+ export declare const QODER_PAT_ENV: readonly ["QODER_PERSONAL_ACCESS_TOKEN", "QODER_PAT"];
13
+ export declare const USER_AGENT = "opencode-qoder";
14
+ export declare const ZERO_COST: Readonly<{
15
+ input: 0;
16
+ output: 0;
17
+ cache_read: 0;
18
+ cache_write: 0;
19
+ }>;
20
+ export type QoderModelDefinition = {
21
+ id: string;
22
+ name: string;
23
+ reasoning: boolean;
24
+ supportsEffort: boolean;
25
+ input: Array<"text" | "image">;
26
+ contextWindow: number;
27
+ maxTokens: number;
28
+ };
29
+ export declare const QODER_MODELS: QoderModelDefinition[];
30
+ export declare function getModelDefinition(modelID: string): QoderModelDefinition;
@@ -0,0 +1,127 @@
1
+ export const PROVIDER_ID = "qoder";
2
+ export const PROVIDER_NAME = "Qoder";
3
+ export const QODER_BASE_URL = "https://api3.qoder.sh/";
4
+ export const QODER_OPENAPI_URL = "https://openapi.qoder.sh";
5
+ export const QODER_CENTER_URL = "https://center.qoder.sh";
6
+ export const QODER_MANAGE_URL = "https://qoder.com";
7
+ export const QODER_MODEL_LIST_URL = `${QODER_BASE_URL}algo/api/v2/model/list`;
8
+ export const QODER_CHAT_URL = `${QODER_BASE_URL}algo/api/v2/service/pro/sse/agent_chat_generation?FetchKeys=llm_model_result&AgentId=agent_common&Encode=1`;
9
+ export const QODER_EXCHANGE_URL = `${QODER_OPENAPI_URL}/api/v1/jobToken/exchange`;
10
+ export const QODER_USERINFO_URL = `${QODER_OPENAPI_URL}/api/v1/userinfo`;
11
+ export const QODER_REFRESH_URL = `${QODER_CENTER_URL}/algo/api/v3/user/refresh_token`;
12
+ export const QODER_PAT_ENV = ["QODER_PERSONAL_ACCESS_TOKEN", "QODER_PAT"];
13
+ export const USER_AGENT = "opencode-qoder";
14
+ export const ZERO_COST = Object.freeze({ input: 0, output: 0, cache_read: 0, cache_write: 0 });
15
+ export const QODER_MODELS = [
16
+ {
17
+ id: "auto",
18
+ name: "Qoder Auto",
19
+ reasoning: true,
20
+ supportsEffort: false,
21
+ input: ["text", "image"],
22
+ contextWindow: 180000,
23
+ maxTokens: 32768,
24
+ },
25
+ {
26
+ id: "ultimate",
27
+ name: "Qoder Ultimate",
28
+ reasoning: true,
29
+ supportsEffort: true,
30
+ input: ["text", "image"],
31
+ contextWindow: 1000000,
32
+ maxTokens: 32768,
33
+ },
34
+ {
35
+ id: "performance",
36
+ name: "Qoder Performance",
37
+ reasoning: true,
38
+ supportsEffort: true,
39
+ input: ["text", "image"],
40
+ contextWindow: 1000000,
41
+ maxTokens: 32768,
42
+ },
43
+ {
44
+ id: "efficient",
45
+ name: "Qoder Efficient",
46
+ reasoning: false,
47
+ supportsEffort: false,
48
+ input: ["text", "image"],
49
+ contextWindow: 180000,
50
+ maxTokens: 32768,
51
+ },
52
+ {
53
+ id: "lite",
54
+ name: "Qoder Lite",
55
+ reasoning: false,
56
+ supportsEffort: false,
57
+ input: ["text"],
58
+ contextWindow: 180000,
59
+ maxTokens: 32768,
60
+ },
61
+ {
62
+ id: "qmodel",
63
+ name: "Qwen3.7 Plus (Qoder)",
64
+ reasoning: false,
65
+ supportsEffort: false,
66
+ input: ["text", "image"],
67
+ contextWindow: 1000000,
68
+ maxTokens: 32768,
69
+ },
70
+ {
71
+ id: "qmodel_latest",
72
+ name: "Qwen3.7 Max (Qoder)",
73
+ reasoning: false,
74
+ supportsEffort: false,
75
+ input: ["text", "image"],
76
+ contextWindow: 1000000,
77
+ maxTokens: 32768,
78
+ },
79
+ {
80
+ id: "dmodel",
81
+ name: "DeepSeek V4 Pro (Qoder)",
82
+ reasoning: true,
83
+ supportsEffort: true,
84
+ input: ["text", "image"],
85
+ contextWindow: 1000000,
86
+ maxTokens: 32768,
87
+ },
88
+ {
89
+ id: "dfmodel",
90
+ name: "DeepSeek V4 Flash (Qoder)",
91
+ reasoning: true,
92
+ supportsEffort: true,
93
+ input: ["text", "image"],
94
+ contextWindow: 1000000,
95
+ maxTokens: 32768,
96
+ },
97
+ {
98
+ id: "gm51model",
99
+ name: "GLM 5.1 (Qoder)",
100
+ reasoning: true,
101
+ supportsEffort: true,
102
+ input: ["text", "image"],
103
+ contextWindow: 180000,
104
+ maxTokens: 32768,
105
+ },
106
+ {
107
+ id: "kmodel",
108
+ name: "Kimi K2.6 (Qoder)",
109
+ reasoning: false,
110
+ supportsEffort: false,
111
+ input: ["text", "image"],
112
+ contextWindow: 256000,
113
+ maxTokens: 32768,
114
+ },
115
+ {
116
+ id: "mmodel",
117
+ name: "MiniMax M3 (Qoder)",
118
+ reasoning: false,
119
+ supportsEffort: false,
120
+ input: ["text", "image"],
121
+ contextWindow: 1000000,
122
+ maxTokens: 32768,
123
+ },
124
+ ];
125
+ export function getModelDefinition(modelID) {
126
+ return QODER_MODELS.find((model) => model.id === modelID) ?? QODER_MODELS[0];
127
+ }
package/dist/cosy.d.ts ADDED
@@ -0,0 +1,9 @@
1
+ export interface CosyCredentials {
2
+ userID: string;
3
+ authToken: string;
4
+ name: string;
5
+ email: string;
6
+ machineID?: string;
7
+ }
8
+ export declare function getMachineId(): string;
9
+ export declare function buildAuthHeaders(body: Buffer | string | null, requestURL: string, creds: CosyCredentials): Record<string, string>;
package/dist/cosy.js ADDED
@@ -0,0 +1,114 @@
1
+ import crypto from "node:crypto";
2
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
+ import { homedir } from "node:os";
4
+ import { dirname, join } from "node:path";
5
+ const qoderRSAPublicKey = `-----BEGIN PUBLIC KEY-----
6
+ MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDA8iMH5c02LilrsERw9t6Pv5Nc
7
+ 4k6Pz1EaDicBMpdpxKduSZu5OANqUq8er4GM95omAGIOPOh+Nx0spthYA2BqGz+l
8
+ 6HRkPJ7S236FZz73In/KVuLnwI8JJ2CbuJap8kvheCCZpmAWpb/cPx/3Vr/J6I17
9
+ XcW+ML9FoCI6AOvOzwIDAQAB
10
+ -----END PUBLIC KEY-----`;
11
+ const QoderIDEVersion = "1.0.0";
12
+ const QoderClientType = "5";
13
+ const QoderDataPolicy = "disagree";
14
+ const QoderLoginVersion = "v2";
15
+ const QoderMachineOS = "x86_64_windows";
16
+ const QoderMachineTypeMagic = "5";
17
+ function rsaEncryptBase64(data) {
18
+ const encrypted = crypto.publicEncrypt({
19
+ key: qoderRSAPublicKey,
20
+ padding: crypto.constants.RSA_PKCS1_PADDING,
21
+ }, typeof data === "string" ? Buffer.from(data) : data);
22
+ return encrypted.toString("base64");
23
+ }
24
+ function aesEncryptCBCBase64(plaintext, keyStr) {
25
+ const cipher = crypto.createCipheriv("aes-128-cbc", Buffer.from(keyStr), Buffer.from(keyStr));
26
+ let encrypted = cipher.update(plaintext, "utf8", "base64");
27
+ encrypted += cipher.final("base64");
28
+ return encrypted;
29
+ }
30
+ function computeSigPath(urlStr) {
31
+ const parsed = new URL(urlStr);
32
+ let sigPath = parsed.pathname;
33
+ if (sigPath.startsWith("/algo"))
34
+ sigPath = sigPath.substring("/algo".length);
35
+ return sigPath;
36
+ }
37
+ export function getMachineId() {
38
+ const paths = [
39
+ join(homedir(), ".qoder", ".auth", "machine_id"),
40
+ join(homedir(), ".local", "share", "opencode", "qoder-machine-id"),
41
+ ];
42
+ for (const path of paths) {
43
+ if (!existsSync(path))
44
+ continue;
45
+ try {
46
+ const val = readFileSync(path, "utf8").trim();
47
+ if (val)
48
+ return val;
49
+ }
50
+ catch { }
51
+ }
52
+ const newId = crypto.randomUUID();
53
+ try {
54
+ const savePath = paths[1];
55
+ mkdirSync(dirname(savePath), { recursive: true });
56
+ writeFileSync(savePath, newId, "utf8");
57
+ }
58
+ catch { }
59
+ return newId;
60
+ }
61
+ export function buildAuthHeaders(body, requestURL, creds) {
62
+ if (!creds.userID)
63
+ throw new Error("qoder: user id is empty");
64
+ if (!creds.authToken)
65
+ throw new Error("qoder: auth token is empty");
66
+ const aesKey = crypto.randomUUID().replace(/-/g, "").slice(0, 16);
67
+ const userInfo = {
68
+ uid: creds.userID,
69
+ security_oauth_token: creds.authToken,
70
+ name: creds.name || "",
71
+ aid: "",
72
+ email: creds.email || "",
73
+ };
74
+ const infoB64 = aesEncryptCBCBase64(JSON.stringify(userInfo), aesKey);
75
+ const cosyKey = rsaEncryptBase64(aesKey);
76
+ const timestamp = Math.floor(Date.now() / 1000).toString();
77
+ const requestId = crypto.randomUUID();
78
+ const cosyPayload = {
79
+ version: "v1",
80
+ requestId,
81
+ info: infoB64,
82
+ cosyVersion: QoderIDEVersion,
83
+ ideVersion: "",
84
+ };
85
+ const payloadB64 = Buffer.from(JSON.stringify(cosyPayload)).toString("base64");
86
+ const sigPath = computeSigPath(requestURL);
87
+ const bodyStr = body ? (Buffer.isBuffer(body) ? body.toString("utf8") : body) : "";
88
+ const sigInput = `${payloadB64}\n${cosyKey}\n${timestamp}\n${bodyStr}\n${sigPath}`;
89
+ const sig = crypto.createHash("md5").update(sigInput).digest("hex");
90
+ const bodyHash = crypto.createHash("md5").update(body || "").digest("hex");
91
+ const bodyLen = body ? (Buffer.isBuffer(body) ? body.length : Buffer.from(body).length).toString() : "0";
92
+ const machineID = creds.machineID || getMachineId();
93
+ return {
94
+ Authorization: `Bearer COSY.${payloadB64}.${sig}`,
95
+ "Cosy-Key": cosyKey,
96
+ "Cosy-User": creds.userID,
97
+ "Cosy-Date": timestamp,
98
+ "Cosy-Version": QoderIDEVersion,
99
+ "Cosy-Machineid": machineID,
100
+ "Cosy-Machinetoken": machineID,
101
+ "Cosy-Machinetype": QoderMachineTypeMagic,
102
+ "Cosy-Machineos": QoderMachineOS,
103
+ "Cosy-Clienttype": QoderClientType,
104
+ "Cosy-Clientip": "127.0.0.1",
105
+ "Cosy-Bodyhash": bodyHash,
106
+ "Cosy-Bodylength": bodyLen,
107
+ "Cosy-Sigpath": sigPath,
108
+ "Cosy-Data-Policy": QoderDataPolicy,
109
+ "Cosy-Organization-Id": "",
110
+ "Cosy-Organization-Tags": "",
111
+ "Login-Version": QoderLoginVersion,
112
+ "X-Request-Id": crypto.randomUUID(),
113
+ };
114
+ }
@@ -0,0 +1 @@
1
+ export declare function qoderEncodeBody(plaintext: string | Buffer): string;
@@ -0,0 +1,19 @@
1
+ const qoderCustomAlphabet = "_doRTgHZBKcGVjlvpC,@aFSx#DPuNJme&i*MzLOEn)sUrthbf%Y^w.(kIQyXqWA!";
2
+ const qoderStdAlphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
3
+ export function qoderEncodeBody(plaintext) {
4
+ const std = Buffer.isBuffer(plaintext) ? plaintext.toString("base64") : Buffer.from(plaintext).toString("base64");
5
+ const n = std.length;
6
+ const a = Math.floor(n / 3);
7
+ const rearranged = std.slice(n - a) + std.slice(a, n - a) + std.slice(0, a);
8
+ let out = "";
9
+ for (let i = 0; i < n; i++) {
10
+ const c = rearranged[i];
11
+ if (c === "=") {
12
+ out += "$";
13
+ continue;
14
+ }
15
+ const idx = qoderStdAlphabet.indexOf(c);
16
+ out += idx >= 0 ? qoderCustomAlphabet[idx] : c;
17
+ }
18
+ return out;
19
+ }
@@ -0,0 +1,11 @@
1
+ import type { Hooks, PluginInput, PluginOptions } from "@opencode-ai/plugin";
2
+ import type { PluginContext } from "@opencode-ai/plugin/v2/promise";
3
+ import { createQoder, QoderLanguageModel } from "./language-model.js";
4
+ export { createQoder, QoderLanguageModel };
5
+ declare function setupV2(ctx: PluginContext): Promise<void>;
6
+ declare const plugin: {
7
+ id: string;
8
+ setup: typeof setupV2;
9
+ server: (_input: PluginInput, options?: PluginOptions) => Promise<Hooks>;
10
+ };
11
+ export default plugin;