From efd20be8d81ef2e256a7037f3aa78e6b567b5fd3 Mon Sep 17 00:00:00 2001 From: Janu Yoga Date: Fri, 19 Jun 2026 15:33:26 +0700 Subject: [PATCH] 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 --- custom-server.js | 13 ++-- open-sse/executors/codebuddy-cn.js | 36 ++++++++++ open-sse/executors/index.js | 3 + open-sse/providers/capabilities.js | 25 ++++++- open-sse/providers/registry/codebuddy-cn.js | 62 ++++++++++++++++++ open-sse/providers/registry/codebuddy.js | 32 --------- open-sse/providers/registry/index.js | 2 +- open-sse/services/tokenRefresh.js | 3 + open-sse/services/tokenRefresh/providers.js | 54 +++++++++++++++ public/providers/codebuddy-cn.png | Bin 0 -> 23433 bytes .../api/oauth/[provider]/[action]/route.js | 4 +- src/app/api/providers/[id]/test/testUtils.js | 2 +- src/lib/oauth/constants/oauth.js | 4 +- src/lib/oauth/providers.js | 17 +++-- src/shared/components/OAuthModal.js | 2 +- tests/__baseline__/alias-baseline.json | 4 +- tests/__baseline__/providers-baseline.json | 4 +- .../golden-url-header.test.js.snap | 26 ++++++-- 18 files changed, 234 insertions(+), 59 deletions(-) create mode 100644 open-sse/executors/codebuddy-cn.js create mode 100644 open-sse/providers/registry/codebuddy-cn.js delete mode 100644 open-sse/providers/registry/codebuddy.js create mode 100644 public/providers/codebuddy-cn.png diff --git a/custom-server.js b/custom-server.js index 764a2df6..6e39683f 100644 --- a/custom-server.js +++ b/custom-server.js @@ -10,10 +10,15 @@ http.createServer = (...args) => { const rest = args.filter((a) => typeof a !== "function"); if (!handler) return origCreate(...args); const wrapped = (req, res) => { - const ip = req.socket && req.socket.remoteAddress ? req.socket.remoteAddress : ""; - // Forwarding headers present = request arrived via a reverse proxy; loopback - // socket is the proxy hop, not the end-user, so it must not be trusted as local. - const viaProxy = !!(req.headers["x-forwarded-for"] || req.headers["x-real-ip"]); + const socketIp = req.socket && req.socket.remoteAddress ? req.socket.remoteAddress : ""; + const xff = req.headers["x-forwarded-for"]; + const xRealIp = req.headers["x-real-ip"]; + const viaProxy = !!(xff || xRealIp); + const isLoopbackProxy = socketIp === "127.0.0.1" || socketIp === "::1" || socketIp === "::ffff:127.0.0.1"; + // Trust forwarding headers only when the TCP peer is a local reverse proxy. + // Direct/public sockets remain keyed by the unspoofable peer address. + const proxyIp = xRealIp || (xff ? String(xff).split(",")[0].trim() : ""); + const ip = isLoopbackProxy && proxyIp ? proxyIp : socketIp; delete req.headers["x-9r-real-ip"]; delete req.headers["x-forwarded-for"]; delete req.headers["x-9r-via-proxy"]; diff --git a/open-sse/executors/codebuddy-cn.js b/open-sse/executors/codebuddy-cn.js new file mode 100644 index 00000000..46e51951 --- /dev/null +++ b/open-sse/executors/codebuddy-cn.js @@ -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; diff --git a/open-sse/executors/index.js b/open-sse/executors/index.js index bac26447..77d12fb3 100644 --- a/open-sse/executors/index.js +++ b/open-sse/executors/index.js @@ -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"; diff --git a/open-sse/providers/capabilities.js b/open-sse/providers/capabilities.js index 77220317..c49536b9 100644 --- a/open-sse/providers/capabilities.js +++ b/open-sse/providers/capabilities.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 diff --git a/open-sse/providers/registry/codebuddy-cn.js b/open-sse/providers/registry/codebuddy-cn.js new file mode 100644 index 00000000..8b12a1a7 --- /dev/null +++ b/open-sse/providers/registry/codebuddy-cn.js @@ -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, + }, +}; diff --git a/open-sse/providers/registry/codebuddy.js b/open-sse/providers/registry/codebuddy.js deleted file mode 100644 index 4bf2c811..00000000 --- a/open-sse/providers/registry/codebuddy.js +++ /dev/null @@ -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, - }, -}; diff --git a/open-sse/providers/registry/index.js b/open-sse/providers/registry/index.js index a45bb1e3..c9dca60d 100644 --- a/open-sse/providers/registry/index.js +++ b/open-sse/providers/registry/index.js @@ -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"; diff --git a/open-sse/services/tokenRefresh.js b/open-sse/services/tokenRefresh.js index 10d8efea..f759493e 100644 --- a/open-sse/services/tokenRefresh.js +++ b/open-sse/services/tokenRefresh.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 }; diff --git a/open-sse/services/tokenRefresh/providers.js b/open-sse/services/tokenRefresh/providers.js index 33d6baf6..b8352156 100644 --- a/open-sse/services/tokenRefresh/providers.js +++ b/open-sse/services/tokenRefresh/providers.js @@ -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: }. +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); +} diff --git a/public/providers/codebuddy-cn.png b/public/providers/codebuddy-cn.png new file mode 100644 index 0000000000000000000000000000000000000000..eae3f4c1752e24c43a98e811a081a06c82e2260e GIT binary patch literal 23433 zcmXt;Wl$Vlw}m0NyCi7P;31H~hXe@@!68_19VEEBI|O%khr!+5Z3dUYgS%bcTlM|u z?y9cp?o;RVS$nT%@6ey}5?JUT(Ba_Vu%sl#l-{n1{~f5vZ)ZVxyDd04TsSGQ?~_j&3+isjUM=5Z3RgK zahh0JCf}A)hsc%|m2bZaC}p#l6*kbX7E&7~LvZQ{B5)oEzTQ~Nl}mnNj4%E772eU) zKxguqX(+2Si`k(yX{2fANvbk&QTtDKpD`qI&|0dzBOuHO63OLJTE?i4)KWHA!5&Em zl|#E}!aphraKpcO8m2VhpNtO`^plZq|01icy@g`-k}gQN zrsO)xL(Y41=)2iXhmbJPAUuGFGb4l414*2tb}0>Ck3f#9?Z6+WRv$&+;_LI;fDDH# zxq4NGe7Vd76nx0Gr{I#QqX(o77DOrh`&(vT+(`~nT|#12Fd1#2rBmKYN2C

LvTR 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 { @@ -1212,23 +1212,25 @@ const PROVIDERS = { }; }, pollToken: async (config, deviceCode) => { - const response = await fetch(config.tokenUrl, { - method: "POST", + // CodeBuddy polls the token endpoint via GET with the state as a query + // param (not POST/body) — matches the official CLI's /v2/plugin/auth/token?state=... + const response = await fetch(`${config.tokenUrl}?state=${encodeURIComponent(deviceCode)}`, { + method: "GET", headers: { - "Content-Type": "application/json", Accept: "application/json", "User-Agent": config.userAgent, "X-Requested-With": "XMLHttpRequest", "X-Domain": "copilot.tencent.com", "X-No-Authorization": "true", "X-No-User-Id": "true", + "X-No-Enterprise-Id": "true", + "X-No-Department-Info": "true", "X-Product": "SaaS", }, - body: JSON.stringify({ state: deviceCode }), }); if (!response.ok) return { ok: false, data: { error: "request_failed" } }; const data = await response.json(); - // code 11217 = pending, code 0 = success + // code 11217 = pending (RetryFetchToken), code 0 = success if (data.code === 0 && data.data?.accessToken) { return { ok: true, @@ -1236,6 +1238,7 @@ const PROVIDERS = { access_token: data.data.accessToken, refresh_token: data.data.refreshToken || "", token_type: data.data.tokenType || "Bearer", + expires_in: data.data.expiresIn, }, }; } @@ -1245,7 +1248,7 @@ const PROVIDERS = { mapTokens: (tokens) => ({ accessToken: tokens.access_token, refreshToken: tokens.refresh_token, - expiresIn: 86400, + expiresIn: tokens.expires_in || 86400, providerSpecificData: {}, }), }, diff --git a/src/shared/components/OAuthModal.js b/src/shared/components/OAuthModal.js index 28879c58..424e23a1 100644 --- a/src/shared/components/OAuthModal.js +++ b/src/shared/components/OAuthModal.js @@ -157,7 +157,7 @@ export default function OAuthModal({ isOpen, provider, providerInfo, onSuccess, setError(null); // Device code flow providers - const deviceCodeProviders = ["github", "qwen", "kiro", "kimi-coding", "kilocode", "codebuddy", "qoder"]; + const deviceCodeProviders = ["github", "qwen", "kiro", "kimi-coding", "kilocode", "codebuddy-cn", "qoder"]; if (deviceCodeProviders.includes(provider)) { setIsDeviceCode(true); setStep("waiting"); diff --git a/tests/__baseline__/alias-baseline.json b/tests/__baseline__/alias-baseline.json index c980af4b..7c6c8baa 100644 --- a/tests/__baseline__/alias-baseline.json +++ b/tests/__baseline__/alias-baseline.json @@ -105,7 +105,7 @@ "claude": "cc", "cline": "cl", "cloudflare-ai": "cloudflare-ai", - "codebuddy": "codebuddy", + "codebuddy-cn": "codebuddy-cn", "codex": "cx", "cohere": "cohere", "commandcode": "commandcode", @@ -231,4 +231,4 @@ "xiaomi-mimo", "xiaomi-tokenplan" ] -} \ No newline at end of file +} diff --git a/tests/__baseline__/providers-baseline.json b/tests/__baseline__/providers-baseline.json index 0265b536..251e221e 100644 --- a/tests/__baseline__/providers-baseline.json +++ b/tests/__baseline__/providers-baseline.json @@ -349,8 +349,8 @@ "baseUrl": "https://gitlab.com/api/v4/chat/completions", "format": "openai" }, - "codebuddy": { - "baseUrl": "https://copilot.tencent.com/v1/chat/completions", + "codebuddy-cn": { + "baseUrl": "https://copilot.tencent.com/v2/chat/completions", "format": "openai" }, "opencode-go": { diff --git a/tests/translator/__snapshots__/golden-url-header.test.js.snap b/tests/translator/__snapshots__/golden-url-header.test.js.snap index 87a0a4b8..9482052c 100644 --- a/tests/translator/__snapshots__/golden-url-header.test.js.snap +++ b/tests/translator/__snapshots__/golden-url-header.test.js.snap @@ -287,21 +287,39 @@ exports[`GOLDEN buildHeaders (default executor providers) > cloudflare-ai → he } `; -exports[`GOLDEN buildHeaders (default executor providers) > codebuddy → headers (apiKey / oauth) 1`] = ` +exports[`GOLDEN buildHeaders (default executor providers) > codebuddy-cn → headers (apiKey / oauth) 1`] = ` { "apiKey": { "Accept": "text/event-stream", "Authorization": "Bearer ", "Content-Type": "application/json", + "User-Agent": "CLI/2.108.1 CodeBuddy/2.108.1", + "X-IDE-Name": "CLI", + "X-IDE-Type": "CLI", + "X-Product": "SaaS", + "x-codebuddy-request": "1", + "x-requested-with": "XMLHttpRequest", }, "nonStream": { "Authorization": "Bearer ", "Content-Type": "application/json", + "User-Agent": "CLI/2.108.1 CodeBuddy/2.108.1", + "X-IDE-Name": "CLI", + "X-IDE-Type": "CLI", + "X-Product": "SaaS", + "x-codebuddy-request": "1", + "x-requested-with": "XMLHttpRequest", }, "oauth": { "Accept": "text/event-stream", "Authorization": "Bearer ", "Content-Type": "application/json", + "User-Agent": "CLI/2.108.1 CodeBuddy/2.108.1", + "X-IDE-Name": "CLI", + "X-IDE-Type": "CLI", + "X-Product": "SaaS", + "x-codebuddy-request": "1", + "x-requested-with": "XMLHttpRequest", }, } `; @@ -1001,10 +1019,10 @@ exports[`GOLDEN buildUrl (default executor providers) > cloudflare-ai → url (s } `; -exports[`GOLDEN buildUrl (default executor providers) > codebuddy → url (stream + non-stream) 1`] = ` +exports[`GOLDEN buildUrl (default executor providers) > codebuddy-cn → url (stream + non-stream) 1`] = ` { - "nonStream": "https://copilot.tencent.com/v1/chat/completions", - "stream": "https://copilot.tencent.com/v1/chat/completions", + "nonStream": "https://copilot.tencent.com/v2/chat/completions", + "stream": "https://copilot.tencent.com/v2/chat/completions", } `;