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:
36
open-sse/executors/codebuddy-cn.js
Normal file
36
open-sse/executors/codebuddy-cn.js
Normal 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;
|
||||
@@ -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";
|
||||
|
||||
@@ -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
|
||||
|
||||
62
open-sse/providers/registry/codebuddy-cn.js
Normal file
62
open-sse/providers/registry/codebuddy-cn.js
Normal 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,
|
||||
},
|
||||
};
|
||||
@@ -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,
|
||||
},
|
||||
};
|
||||
@@ -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";
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user