feat(provider): add CodeBuddy CN provider (copilot.tencent.com)

Add Tencent CodeBuddy CN (codebuddy-cn) OAuth provider with full support:
OAuth login (GET poll with state query param), token refresh, 15-model
catalog, /v2 inference endpoint, forced streaming, OpenAI-style reasoning,
and per-model capabilities. Renamed from codebuddy to codebuddy-cn to allow
a future codebuddy-ai variant.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Janu Yoga
2026-06-19 15:33:26 +07:00
committed by decolua
parent db4499d6df
commit efd20be8d8
18 changed files with 234 additions and 59 deletions

View File

@@ -0,0 +1,36 @@
import { DefaultExecutor } from "./default.js";
/**
* CodeBuddyExecutor — talks to https://copilot.tencent.com/v2/chat/completions
*
* CodeBuddy is OpenAI-compatible but rejects non-stream chat requests
* (HTTP 400, code 11101 "Non-stream chat request is currently not supported").
* The same-format (openai→openai) translator path leaves body.stream as the
* client sent it, so we force it true here — 9router still re-aggregates the
* SSE into a JSON response for non-streaming clients.
*/
export class CodeBuddyExecutor extends DefaultExecutor {
constructor() {
super("codebuddy-cn");
}
transformRequest(model, body, stream, credentials) {
const transformed = super.transformRequest(model, body, stream, credentials);
transformed.stream = true;
// CodeBuddy only surfaces model reasoning when the request carries the CLI's
// OpenAI-style params: reasoning_effort + reasoning_summary:"auto". 9router's
// thinking pipeline sets reasoning_effort only when the client asks, and never
// sets reasoning_summary — so reasoning never shows. Mirror the CLI here.
const eff = transformed.reasoning_effort;
if (eff === "none" || eff === "off") {
delete transformed.reasoning_effort; // gateway has no "none" — just omit
} else {
if (!eff) transformed.reasoning_effort = "medium";
transformed.reasoning_summary = "auto";
}
return transformed;
}
}
export default CodeBuddyExecutor;

View File

@@ -17,6 +17,7 @@ import { OllamaLocalExecutor } from "./ollama-local.js";
import { CommandCodeExecutor } from "./commandcode.js";
import { XiaomiTokenplanExecutor } from "./xiaomi-tokenplan.js";
import { MimoFreeExecutor } from "./mimo-free.js";
import { CodeBuddyExecutor } from "./codebuddy-cn.js";
import { DefaultExecutor } from "./default.js";
const executors = {
@@ -42,6 +43,7 @@ const executors = {
"xiaomi-tokenplan": new XiaomiTokenplanExecutor(),
"mimo-free": new MimoFreeExecutor(),
mmf: new MimoFreeExecutor(), // Alias for mimo-free
"codebuddy-cn": new CodeBuddyExecutor(),
};
const defaultCache = new Map();
@@ -77,3 +79,4 @@ export { OllamaLocalExecutor } from "./ollama-local.js";
export { CommandCodeExecutor } from "./commandcode.js";
export { XiaomiTokenplanExecutor } from "./xiaomi-tokenplan.js";
export { MimoFreeExecutor } from "./mimo-free.js";
export { CodeBuddyExecutor } from "./codebuddy-cn.js";

View File

@@ -92,7 +92,30 @@ export const MODEL_CAPABILITIES = {
/**
* Provider-specific capability overrides. Keyed by provider alias/id.
*/
export const PROVIDER_CAPABILITIES = {};
export const PROVIDER_CAPABILITIES = {
// CodeBuddy.cn — authoritative per-model metadata from the gateway's model
// config (contextWindow=maxInputTokens, maxOutput=maxOutputTokens, vision=
// supportsImages). Every model reasons via OpenAI-style reasoning_effort
// (see registry thinkingFormat). `onlyReasoning` models can't turn thinking
// off → thinkingCanDisable:false (clamped to minimal instead of disabled).
"codebuddy-cn": {
"glm-5.2": { reasoning: true, thinkingFormat: "openai", thinkingCanDisable: false, contextWindow: 1000000, maxOutput: 48000 },
"glm-5.1": { reasoning: true, thinkingFormat: "openai", thinkingCanDisable: false, contextWindow: 200000, maxOutput: 48000 },
"glm-5.0": { reasoning: true, thinkingFormat: "openai", contextWindow: 200000, maxOutput: 48000 },
"glm-5.0-turbo": { reasoning: true, thinkingFormat: "openai", thinkingCanDisable: false, contextWindow: 200000, maxOutput: 48000 },
"glm-5v-turbo": { vision: true, reasoning: true, thinkingFormat: "openai", thinkingCanDisable: false, contextWindow: 200000, maxOutput: 38000 },
"glm-4.7": { reasoning: true, thinkingFormat: "openai", contextWindow: 200000, maxOutput: 48000 },
"minimax-m3": { vision: true, reasoning: true, thinkingFormat: "openai", thinkingCanDisable: false, contextWindow: 512000, maxOutput: 48000 },
"minimax-m2.7": { vision: true, reasoning: true, thinkingFormat: "openai", thinkingCanDisable: false, contextWindow: 200000, maxOutput: 48000 },
"kimi-k2.7": { vision: true, reasoning: true, thinkingFormat: "openai", thinkingCanDisable: false, contextWindow: 256000, maxOutput: 32000 },
"kimi-k2.6": { vision: true, reasoning: true, thinkingFormat: "openai", thinkingCanDisable: false, contextWindow: 256000, maxOutput: 32000 },
"kimi-k2.5": { vision: true, reasoning: true, thinkingFormat: "openai", thinkingCanDisable: false, contextWindow: 164000, maxOutput: 32000 },
"hy3-preview": { vision: true, reasoning: true, thinkingFormat: "openai", thinkingCanDisable: false, contextWindow: 192000, maxOutput: 64000 },
"deepseek-v4-pro": { vision: true, reasoning: true, thinkingFormat: "openai", thinkingCanDisable: false, contextWindow: 1000000, maxOutput: 50000 },
"deepseek-v4-flash": { vision: true, reasoning: true, thinkingFormat: "openai", thinkingCanDisable: false, contextWindow: 1000000, maxOutput: 50000 },
"deepseek-v3-2-volc": { reasoning: true, thinkingFormat: "openai", thinkingCanDisable: false, contextWindow: 96000, maxOutput: 32000 },
},
};
/**
* Pattern fallback — glob (* = wildcard), matched case-insensitively and

View File

@@ -0,0 +1,62 @@
export default {
id: "codebuddy-cn",
hidden: false,
priority: 90,
display: {
name: "CodeBuddy CN",
icon: "smart_toy",
color: "#006EFF",
website: "https://copilot.tencent.com",
notice: {
signupUrl: "https://copilot.tencent.com",
},
},
category: "oauth",
transport: {
baseUrl: "https://copilot.tencent.com/v2/chat/completions",
forceStream: true,
// CodeBuddy is a unified OpenAI-compatible gateway: every model (GLM, Kimi,
// MiniMax, DeepSeek, Hunyuan) takes reasoning via OpenAI-style reasoning_effort,
// not its vendor-native thinking shape. Force the openai thinking format.
thinkingFormat: "openai",
headers: {
"User-Agent": "CLI/2.108.1 CodeBuddy/2.108.1",
"X-Product": "SaaS",
"X-IDE-Type": "CLI",
"X-IDE-Name": "CLI",
"x-requested-with": "XMLHttpRequest",
"x-codebuddy-request": "1",
},
auth: {
combined: true,
header: "Authorization",
scheme: "bearer",
},
},
models: [
{ id: "glm-5.2", name: "GLM-5.2" },
{ id: "glm-5.1", name: "GLM-5.1" },
{ id: "glm-5.0", name: "GLM-5.0" },
{ id: "glm-5.0-turbo", name: "GLM-5.0-Turbo" },
{ id: "glm-5v-turbo", name: "GLM-5v-Turbo" },
{ id: "glm-4.7", name: "GLM-4.7" },
{ id: "minimax-m3", name: "MiniMax-M3" },
{ id: "minimax-m2.7", name: "MiniMax-M2.7" },
{ id: "kimi-k2.7", name: "Kimi-K2.7-Code" },
{ id: "kimi-k2.6", name: "Kimi-K2.6" },
{ id: "kimi-k2.5", name: "Kimi-K2.5" },
{ id: "hy3-preview", name: "Hy3 Preview" },
{ id: "deepseek-v4-pro", name: "DeepSeek-V4-Pro" },
{ id: "deepseek-v4-flash", name: "DeepSeek-V4-Flash" },
{ id: "deepseek-v3-2-volc", name: "DeepSeek-V3.2" },
],
oauth: {
baseUrl: "https://copilot.tencent.com",
stateUrl: "https://copilot.tencent.com/v2/plugin/auth/state",
tokenUrl: "https://copilot.tencent.com/v2/plugin/auth/token",
refreshUrl: "https://copilot.tencent.com/v2/plugin/auth/token/refresh",
userAgent: "CLI/2.63.2 CodeBuddy/2.63.2",
platform: "CLI",
pollInterval: 5000,
},
};

View File

@@ -1,32 +0,0 @@
export default {
id: "codebuddy",
hidden: true,
priority: 90,
display: {
name: "CodeBuddy",
icon: "smart_toy",
color: "#006EFF",
website: "https://copilot.tencent.com",
notice: {
signupUrl: "https://copilot.tencent.com",
},
},
category: "oauth",
transport: {
baseUrl: "https://copilot.tencent.com/v1/chat/completions",
auth: {
combined: true,
header: "Authorization",
scheme: "bearer",
},
},
oauth: {
baseUrl: "https://copilot.tencent.com",
stateUrl: "https://copilot.tencent.com/v2/plugin/auth/state",
tokenUrl: "https://copilot.tencent.com/v2/plugin/auth/token",
refreshUrl: "https://copilot.tencent.com/v2/plugin/auth/token/refresh",
userAgent: "CLI/2.63.2 CodeBuddy/2.63.2",
platform: "CLI",
pollInterval: 5000,
},
};

View File

@@ -16,7 +16,7 @@ import p13 from "./chutes.js";
import p14 from "./claude.js";
import p15 from "./cline.js";
import p16 from "./cloudflare-ai.js";
import p17 from "./codebuddy.js";
import p17 from "./codebuddy-cn.js";
import p18 from "./codex.js";
import p19 from "./cohere.js";
import p20 from "./comfyui.js";

View File

@@ -11,6 +11,7 @@ import {
refreshIflowToken,
refreshGitHubToken,
refreshCopilotToken,
refreshCodebuddyToken,
classifyOAuthRefreshError,
} from "./tokenRefresh/providers.js";
@@ -25,6 +26,7 @@ export {
refreshIflowToken,
refreshGitHubToken,
refreshCopilotToken,
refreshCodebuddyToken,
classifyOAuthRefreshError,
};
@@ -127,6 +129,7 @@ const REFRESH_HANDLERS = {
github: (c, log) => refreshGitHubToken(c.refreshToken, log),
kiro: (c, log) => refreshKiroToken(c.refreshToken, c.providerSpecificData, log),
xai: (c, log) => refreshXaiToken(c.refreshToken, log),
"codebuddy-cn": (c, log) => refreshCodebuddyToken(c.refreshToken, log),
vertex: vertexRefreshHandler,
"vertex-partner": vertexRefreshHandler
};

View File

@@ -524,3 +524,57 @@ export async function refreshCopilotToken(githubAccessToken, log) {
}
}, log);
}
// CodeBuddy (Tencent) refresh — POST /v2/plugin/auth/token/refresh with the
// refresh token carried in the X-Refresh-Token header (not a form body),
// matching the official CodeBuddy CLI. Response: { code: 0, data: <token> }.
export async function refreshCodebuddyToken(refreshToken, log) {
if (!refreshToken) return null;
return dedupRefresh("codebuddy-cn", refreshToken, async () => {
const oauth = PROVIDER_OAUTH["codebuddy-cn"] || {};
const response = await fetch(oauth.refreshUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
"User-Agent": oauth.userAgent,
"X-Requested-With": "XMLHttpRequest",
"X-Domain": "copilot.tencent.com",
"X-Refresh-Token": refreshToken,
"X-Auth-Refresh-Source": "plugin",
"X-Product": "SaaS",
},
body: "{}",
});
if (!response.ok) {
const errorText = await response.text();
log?.error?.("TOKEN_REFRESH", "Failed to refresh CodeBuddy token", {
status: response.status,
error: errorText,
});
return null;
}
const data = await response.json();
if (data.code !== 0 || !data.data?.accessToken) {
log?.error?.("TOKEN_REFRESH", "CodeBuddy token refresh returned no token", {
code: data.code,
msg: data.msg,
});
return null;
}
log?.info?.("TOKEN_REFRESH", "Successfully refreshed CodeBuddy token", {
hasNewAccessToken: !!data.data.accessToken,
hasNewRefreshToken: !!data.data.refreshToken,
expiresIn: data.data.expiresIn,
});
return {
accessToken: data.data.accessToken,
refreshToken: data.data.refreshToken || refreshToken,
expiresIn: data.data.expiresIn,
};
}, log);
}