From 039c4dbc7274a96a14d850d81f1911eccdfc8777 Mon Sep 17 00:00:00 2001 From: decolua Date: Thu, 23 Jul 2026 15:37:24 +0700 Subject: [PATCH] feat(providers): add trae/windsurf/zed/workbuddy/codebuddy-intl + icons MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - New providers: trae, windsurf, zed, workbuddy, codebuddy-intl (registry + executor, wired into executors/index.js + registry/index.js) - zed: port hosted cloud proxy from OmniRoute — RSA access-token → short-lived LLM token exchange (shared/zedAuth.js) + NDJSON {event}/{status}/[DONE] stream translated back to OpenAI via Claude/Gemini/OpenAI-Responses translators - Provider icons (128x128 png) for the 5 new providers - qoder + tokenRefresh provider tweaks Co-Authored-By: Claude Fable 5 --- open-sse/executors/codebuddy-intl.js | 30 ++ open-sse/executors/index.js | 15 + open-sse/executors/trae.js | 22 + open-sse/executors/windsurf.js | 42 ++ open-sse/executors/workbuddy.js | 31 ++ open-sse/executors/zed.js | 305 +++++++++++++ open-sse/providers/registry/codebuddy-intl.js | 74 ++++ open-sse/providers/registry/index.js | 10 + open-sse/providers/registry/qoder.js | 28 +- open-sse/providers/registry/trae.js | 77 ++++ open-sse/providers/registry/windsurf.js | 146 ++++++ open-sse/providers/registry/workbuddy.js | 73 +++ open-sse/providers/registry/zed.js | 72 +++ open-sse/services/tokenRefresh.js | 15 + open-sse/services/tokenRefresh/providers.js | 195 ++++++++ open-sse/shared/zedAuth.js | 416 ++++++++++++++++++ public/providers/codebuddy-intl.png | Bin 0 -> 17688 bytes public/providers/trae.png | Bin 0 -> 2328 bytes public/providers/windsurf.png | Bin 0 -> 4193 bytes public/providers/workbuddy.png | Bin 0 -> 17688 bytes public/providers/zed.png | Bin 0 -> 21456 bytes 21 files changed, 1539 insertions(+), 12 deletions(-) create mode 100644 open-sse/executors/codebuddy-intl.js create mode 100644 open-sse/executors/trae.js create mode 100644 open-sse/executors/windsurf.js create mode 100644 open-sse/executors/workbuddy.js create mode 100644 open-sse/executors/zed.js create mode 100644 open-sse/providers/registry/codebuddy-intl.js create mode 100644 open-sse/providers/registry/trae.js create mode 100644 open-sse/providers/registry/windsurf.js create mode 100644 open-sse/providers/registry/workbuddy.js create mode 100644 open-sse/providers/registry/zed.js create mode 100644 open-sse/shared/zedAuth.js create mode 100644 public/providers/codebuddy-intl.png create mode 100644 public/providers/trae.png create mode 100644 public/providers/windsurf.png create mode 100644 public/providers/workbuddy.png create mode 100644 public/providers/zed.png diff --git a/open-sse/executors/codebuddy-intl.js b/open-sse/executors/codebuddy-intl.js new file mode 100644 index 00000000..06fe4326 --- /dev/null +++ b/open-sse/executors/codebuddy-intl.js @@ -0,0 +1,30 @@ +import { DefaultExecutor } from "./default.js"; + +/** + * CodeBuddyIntlExecutor — talks to https://www.codebuddy.ai/v2/chat/completions + * + * Same OpenAI-compatible-but-stream-only gateway behavior as codebuddy-cn: + * non-stream requests are rejected, and reasoning is surfaced only when the + * request carries the IDE's OpenAI-style reasoning params. Force stream and + * mirror reasoning_summary exactly like CodeBuddyExecutor. + */ +export class CodeBuddyIntlExecutor extends DefaultExecutor { + constructor() { + super("codebuddy-intl"); + } + + transformRequest(model, body, stream, credentials) { + const transformed = super.transformRequest(model, body, stream, credentials); + transformed.stream = true; + + const eff = transformed.reasoning_effort; + if (eff === "none" || eff === "off") { + delete transformed.reasoning_effort; + } else if (eff) { + transformed.reasoning_summary = "auto"; + } + return transformed; + } +} + +export default CodeBuddyIntlExecutor; diff --git a/open-sse/executors/index.js b/open-sse/executors/index.js index b6091b9d..c9340c10 100644 --- a/open-sse/executors/index.js +++ b/open-sse/executors/index.js @@ -20,6 +20,11 @@ import { CommandCodeExecutor } from "./commandcode.js"; import { XiaomiTokenplanExecutor } from "./xiaomi-tokenplan.js"; import { MimoFreeExecutor } from "./mimo-free.js"; import { CodeBuddyExecutor } from "./codebuddy-cn.js"; +import { CodeBuddyIntlExecutor } from "./codebuddy-intl.js"; +import { WorkBuddyExecutor } from "./workbuddy.js"; +import TraeExecutor from "./trae.js"; +import ZedExecutor from "./zed.js"; +import WindsurfExecutor from "./windsurf.js"; import { DefaultExecutor } from "./default.js"; const executors = { @@ -50,6 +55,11 @@ const executors = { "mimo-free": new MimoFreeExecutor(), mmf: new MimoFreeExecutor(), // Alias for mimo-free "codebuddy-cn": new CodeBuddyExecutor(), + "codebuddy-intl": new CodeBuddyIntlExecutor(), + workbuddy: new WorkBuddyExecutor(), + trae: new TraeExecutor(), + zed: new ZedExecutor(), + windsurf: new WindsurfExecutor(), }; const defaultCache = new Map(); @@ -88,3 +98,8 @@ export { CommandCodeExecutor } from "./commandcode.js"; export { XiaomiTokenplanExecutor } from "./xiaomi-tokenplan.js"; export { MimoFreeExecutor } from "./mimo-free.js"; export { CodeBuddyExecutor } from "./codebuddy-cn.js"; +export { CodeBuddyIntlExecutor } from "./codebuddy-intl.js"; +export { WorkBuddyExecutor } from "./workbuddy.js"; +export { default as TraeExecutor } from "./trae.js"; +export { default as ZedExecutor } from "./zed.js"; +export { default as WindsurfExecutor } from "./windsurf.js"; diff --git a/open-sse/executors/trae.js b/open-sse/executors/trae.js new file mode 100644 index 00000000..347d5307 --- /dev/null +++ b/open-sse/executors/trae.js @@ -0,0 +1,22 @@ +import { DefaultExecutor } from "./default.js"; + +// Trae executor — inject x-cloudide-token (raw access token) + Authorization Bearer. +// Mirrors trae_account.rs request_trae_json header set. +export default class TraeExecutor extends DefaultExecutor { + constructor() { + super("trae"); + } + + buildHeaders(credentials, stream = true) { + const headers = super.buildHeaders(credentials, stream); + const token = credentials?.accessToken; + if (token) { + // Raw token (no Bearer prefix) on x-cloudide-token — matches official client. + headers["x-cloudide-token"] = token; + headers["Authorization"] = `Bearer ${token}`; + } + return headers; + } + + // TODO verify: if Chat is JSON-RPC shaped, override transformRequest here. +} diff --git a/open-sse/executors/windsurf.js b/open-sse/executors/windsurf.js new file mode 100644 index 00000000..d9b19ec3 --- /dev/null +++ b/open-sse/executors/windsurf.js @@ -0,0 +1,42 @@ +import { DefaultExecutor } from "./default.js"; + +// Windsurf chat = Codeium binary protobuf gRPC-Web. +// The .proto schema for exa.server_pb.ServerService is NOT in either source +// repo, so request/response encode+decode cannot be implemented truthfully. +// Auth, headers, quota are wired; the chat payload is intentionally a hard +// failure rather than a fabricated protobuf body. +export class WindsurfExecutor extends DefaultExecutor { + constructor() { + super("windsurf"); + } + + buildHeaders(credentials, stream = true) { + const headers = { + "Content-Type": "application/proto", + "Connect-Protocol-Version": "1", + ideName: "Windsurf", + extensionName: "codeium.windsurf", + ...(this.config.headers || {}), + }; + // apiKey from RegisterUser (sk-ws-..., Firebase-derived, or Devin ide_token). + const token = credentials?.apiKey || credentials?.accessToken; + if (token) headers["Authorization"] = `Bearer ${token}`; + return headers; + } + + // TODO(proto): implement once Codeium server_pb .proto is recovered. + // - encode request: chat history + model + system → protobuf bytes + // - decode response: stream protobuf frames → OpenAI-shaped chunks + async execute() { + throw new Error( + "Windsurf chat (Codeium protobuf) not yet implemented — needs .proto schema. Auth/quota wired." + ); + } + + async refreshCredentials() { + // Windsurf apiKey is long-lived (like cursor); refresh handled out-of-band. + return null; + } +} + +export default WindsurfExecutor; diff --git a/open-sse/executors/workbuddy.js b/open-sse/executors/workbuddy.js new file mode 100644 index 00000000..b306b18d --- /dev/null +++ b/open-sse/executors/workbuddy.js @@ -0,0 +1,31 @@ +import { DefaultExecutor } from "./default.js"; + +/** + * WorkBuddyExecutor — talks to https://www.codebuddy.cn/v2/chat/completions + * + * WorkBuddy is a B2B/enterprise skin of CodeBuddy CN (same codebuddy.cn + * OpenAI-compatible gateway). Behavior mirrors CodeBuddyExecutor: + * gateway rejects non-stream requests, and reasoning must be surfaced via + * OpenAI-style reasoning_effort + reasoning_summary:"auto" (vendor-native + * thinking shapes are not honored by the unified gateway). + */ +export class WorkBuddyExecutor extends DefaultExecutor { + constructor() { + super("workbuddy"); + } + + transformRequest(model, body, stream, credentials) { + const transformed = super.transformRequest(model, body, stream, credentials); + transformed.stream = true; + + const eff = transformed.reasoning_effort; + if (eff === "none" || eff === "off") { + delete transformed.reasoning_effort; + } else if (eff) { + transformed.reasoning_summary = "auto"; + } + return transformed; + } +} + +export default WorkBuddyExecutor; diff --git a/open-sse/executors/zed.js b/open-sse/executors/zed.js new file mode 100644 index 00000000..d7ffb401 --- /dev/null +++ b/open-sse/executors/zed.js @@ -0,0 +1,305 @@ +// ZedHostedExecutor — routes requests to Zed's hosted LLM aggregator +// (cloud.zed.dev/completions), a multi-format proxy fronting +// Anthropic/OpenAI/Google/xAI depending on the requested model. +// +// Wire protocol: POST /completions with an NDJSON/SSE-ish body-per-line +// response stream (`{"event": }` / `{"status": ...}` / +// `[DONE]`), authenticated with a short-lived LLM bearer token exchanged from +// the RSA-decrypted access_token (see open-sse/shared/zedAuth.js). The +// provider-shaped chunk is Claude/Gemini/OpenAI-Responses/xAI(OpenAI-shaped) +// depending on which upstream Zed fronts for the model — translated back to +// OpenAI Chat Completions by reusing the existing translators. +// +// Ported from OmniRoute open-sse/executors/zed-hosted.ts. Overrides execute() +// entirely (does NOT use DefaultExecutor's pipeline) because the Zed wire +// shape (thread envelope, LLM-token exchange, NDJSON status frames) doesn't +// fit the generic transformRequest/buildUrl contract. + +import { BaseExecutor } from "./base.js"; +import { FORMATS } from "../translator/formats.js"; +import { initState } from "../translator/index.js"; +import { openaiToClaudeRequest } from "../translator/request/openai-to-claude.js"; +import { openaiToGeminiRequest } from "../translator/request/openai-to-gemini.js"; +import { openaiToOpenAIResponsesRequest } from "../translator/request/openai-responses.js"; +import { claudeToOpenAIResponse } from "../translator/response/claude-to-openai.js"; +import { geminiToOpenAIResponse } from "../translator/response/gemini-to-openai.js"; +import { openaiResponsesToOpenAIResponse } from "../translator/response/openai-responses.js"; +import { + ZED_HEADERS, + resolveZedModels, + zedLlmFetch, +} from "../shared/zedAuth.js"; + +const ZED_PROVIDER = { + anthropic: "Anthropic", + openai: "OpenAi", + google: "Google", + xai: "XAi", +}; + +function normalizeZedProvider(value, model) { + const raw = String(value || "").toLowerCase(); + if (raw === "anthropic") return ZED_PROVIDER.anthropic; + if (raw === "openai" || raw === "open_ai") return ZED_PROVIDER.openai; + if (raw === "google" || raw === "gemini") return ZED_PROVIDER.google; + if (raw === "xai" || raw === "x_ai" || raw === "x-ai") return ZED_PROVIDER.xai; + + const m = String(model || "").toLowerCase(); + if (m.includes("claude")) return ZED_PROVIDER.anthropic; + if (m.includes("gemini")) return ZED_PROVIDER.google; + if (m.includes("grok") || m.includes("xai")) return ZED_PROVIDER.xai; + return ZED_PROVIDER.openai; +} + +function buildProviderRequest(provider, model, body, stream, credentials) { + if (provider === ZED_PROVIDER.anthropic) { + return openaiToClaudeRequest(model, body, true); + } + if (provider === ZED_PROVIDER.google) { + return openaiToGeminiRequest(model, body, true); + } + if (provider === ZED_PROVIDER.openai) { + return openaiToOpenAIResponsesRequest(model, body, true, credentials); + } + // xAI is OpenAI-shaped — forward as-is. + return { ...(body || {}), model, stream: stream !== false }; +} + +function initProviderState(provider, model) { + if (provider === ZED_PROVIDER.anthropic) return initState(FORMATS.CLAUDE); + if (provider === ZED_PROVIDER.google) return initState(FORMATS.GEMINI); + if (provider === ZED_PROVIDER.openai) return initState(FORMATS.OPENAI_RESPONSES); + const state = initState(FORMATS.OPENAI); + state.model = model; + return state; +} + +function convertProviderEvent(provider, event, state) { + if (provider === ZED_PROVIDER.anthropic) return claudeToOpenAIResponse(event, state); + if (provider === ZED_PROVIDER.google) return geminiToOpenAIResponse(event, state); + if (provider === ZED_PROVIDER.openai) return openaiResponsesToOpenAIResponse(event, state); + return event; +} + +function createErrorChunk(model, message) { + return { + id: `chatcmpl-zed-error-${Date.now()}`, + object: "chat.completion.chunk", + created: Math.floor(Date.now() / 1000), + model, + choices: [ + { index: 0, delta: { content: `[Zed error] ${message}` }, finish_reason: "stop" }, + ], + }; +} + +function enqueueSseObject(controller, encoder, chunk) { + if (!chunk) return; + const items = Array.isArray(chunk) ? chunk : [chunk]; + for (const item of items) { + if (!item) continue; + controller.enqueue(encoder.encode(`data: ${JSON.stringify(item)}\n\n`)); + } +} + +function unwrapZedLine(line) { + let text = line.replace(/\r$/, "").trim(); + if (!text) return null; + if (text.startsWith("data:")) text = text.slice(5).trimStart(); + if (text === "[DONE]") return { done: true }; + try { + const parsed = JSON.parse(text); + if (parsed && Object.prototype.hasOwnProperty.call(parsed, "event")) { + return { event: parsed.event }; + } + if (parsed && Object.prototype.hasOwnProperty.call(parsed, "status")) { + return { status: parsed.status }; + } + return { event: parsed }; + } catch { + return null; + } +} + +function normalizeStatus(status) { + if (!status) return null; + if (typeof status === "string") return { type: status }; + if (typeof status === "object") { + const key = Object.keys(status)[0]; + if (key && typeof status[key] === "object") return { type: key, ...status[key] }; + return status; + } + return null; +} + +function wrapZedCompletionStream(response, provider, model) { + if (!response.ok || !response.body) return response; + + const decoder = new TextDecoder(); + const encoder = new TextEncoder(); + const state = initProviderState(provider, model); + let buffer = ""; + let done = false; + + const finish = (controller) => { + if (done) return; + const finalChunk = convertProviderEvent(provider, null, state); + enqueueSseObject(controller, encoder, finalChunk); + controller.enqueue(encoder.encode("data: [DONE]\n\n")); + done = true; + }; + + const processLine = (line, controller) => { + if (done) return; + const payload = unwrapZedLine(line); + if (!payload) return; + if (payload.done) { + finish(controller); + return; + } + if (payload.status) { + const status = normalizeStatus(payload.status); + if (status?.type === "failed" || status?.failed) { + const failed = status.failed || status; + const message = String(failed.message || failed.error || failed.code || "request failed"); + enqueueSseObject(controller, encoder, createErrorChunk(model, message)); + finish(controller); + } else if (status?.type === "stream_ended" || status === "stream_ended") { + finish(controller); + } + return; + } + const converted = convertProviderEvent(provider, payload.event, state); + enqueueSseObject(controller, encoder, converted); + }; + + const transformed = response.body.pipeThrough( + new TransformStream({ + transform(chunk, controller) { + buffer += decoder.decode(chunk, { stream: true }); + let nl; + while ((nl = buffer.indexOf("\n")) !== -1) { + const line = buffer.slice(0, nl); + buffer = buffer.slice(nl + 1); + processLine(line, controller); + } + }, + flush(controller) { + buffer += decoder.decode(); + if (buffer) { + processLine(buffer, controller); + buffer = ""; + } + finish(controller); + }, + }), + ); + + return new Response(transformed, { + status: response.status, + statusText: response.statusText, + headers: { + "Content-Type": "text/event-stream", + "Cache-Control": "no-cache", + }, + }); +} + +class ZedExecutor extends BaseExecutor { + constructor() { + super("zed"); + } + + async resolveModel(model, credentials, signal, log) { + try { + const catalog = await resolveZedModels(credentials, { config: this.config, signal }); + let raw = catalog?.rawById?.get(model) ?? null; + if (!raw) { + const refreshed = await resolveZedModels(credentials, { + config: this.config, + signal, + forceRefresh: true, + }); + raw = refreshed?.rawById?.get(model) ?? null; + } + return { raw, provider: normalizeZedProvider(raw?.provider, model) }; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + log?.warn?.("ZED", `model catalog unavailable, inferring provider for ${model}: ${message}`); + return { raw: null, provider: normalizeZedProvider(null, model) }; + } + } + + async execute({ model, body, stream, credentials, signal, log, proxyOptions = null }) { + const { provider } = await this.resolveModel(model, credentials, signal, log); + const providerRequest = buildProviderRequest(provider, model, body, stream, credentials); + const bodyRecord = body || {}; + const payload = { + thread_id: bodyRecord.thread_id || credentials?._clientSessionId, + prompt_id: bodyRecord.prompt_id, + provider, + model, + provider_request: providerRequest, + }; + + const response = await zedLlmFetch(credentials, "/completions", { + config: this.config, + signal, + fetchOptions: { + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "application/x-ndjson, text/event-stream, */*", + "User-Agent": "9router/zed", + "x-zed-version": this.config?.appVersion?.toString() || "0.200.0", + [ZED_HEADERS.clientSupportsStatus]: "true", + [ZED_HEADERS.clientSupportsStreamEnded]: "true", + }, + body: JSON.stringify(payload), + }, + }); + + const wrapped = response.ok ? wrapZedCompletionStream(response, provider, model) : response; + return { + response: wrapped, + url: `${this.config?.llmBaseUrl || "https://cloud.zed.dev"}/completions`, + headers: { "Content-Type": "application/json", Authorization: "Bearer " }, + transformedBody: payload, + }; + } + + parseError(response, bodyText) { + let parsed = null; + try { + parsed = JSON.parse(bodyText || "{}"); + } catch { + parsed = null; + } + + const errorObj = parsed?.error || undefined; + const code = parsed?.code || errorObj?.code || ""; + const rawMessage = + parsed?.message || errorObj?.message || bodyText || response.statusText; + if (code === "trial_blocked") { + return { + status: response.status, + message: `Zed trial access is blocked upstream. The account can list hosted models, but Zed is refusing completions until trial/billing access is enabled or unblocked. Zed says: ${rawMessage}`, + }; + } + if (code) { + return { status: response.status, message: `Zed ${code}: ${rawMessage}` }; + } + return { status: response.status, message: rawMessage || `Zed upstream error: ${response.status}` }; + } + + async refreshCredentials() { + // Zed uses a long-lived RSA-decrypted access_token — no OAuth refresh. + return null; + } + + needsRefresh() { + return false; + } +} + +export default ZedExecutor; diff --git a/open-sse/providers/registry/codebuddy-intl.js b/open-sse/providers/registry/codebuddy-intl.js new file mode 100644 index 00000000..4faaf804 --- /dev/null +++ b/open-sse/providers/registry/codebuddy-intl.js @@ -0,0 +1,74 @@ +// CodeBuddy international (codebuddy.ai) — mirrors codebuddy-cn registry shape, +// swapping the Tencent CN domain for the .ai endpoint set discovered in +// cockpit-tools/src-tauri/src/modules/codebuddy_oauth.rs. All OAuth/plugin URLs +// use the /v2/plugin prefix with platform=ide (CN uses platform=CLI). +export default { + id: "codebuddy-intl", + alias: "cbai", + uiAlias: "cbai", + hidden: false, + priority: 90, + display: { + name: "CodeBuddy", + icon: "smart_toy", + color: "#006EFF", + website: "https://www.codebuddy.ai", + notice: { + signupUrl: "https://www.codebuddy.ai", + }, + }, + category: "oauth", + authModes: ["oauth", "apikey"], + hasOAuth: true, + transport: { + // Chat gateway is OpenAI-compatible SSE (same /v2/chat/completions path as CN). + baseUrl: "https://www.codebuddy.ai/v2/chat/completions", + forceStream: true, + // CodeBuddy intl speaks the same unified OpenAI reasoning_effort shape as CN. + thinkingFormat: "openai", + headers: { + "User-Agent": "IDE/2.108.1 CodeBuddy/2.108.1", + "X-Product": "SaaS", + "X-IDE-Type": "IDE", + "X-IDE-Name": "IDE", + "x-requested-with": "XMLHttpRequest", + "x-codebuddy-request": "1", + }, + auth: { + combined: true, + header: "Authorization", + scheme: "bearer", + }, + }, + // Same model lineup exposed by the CN gateway — intl backend is the same catalog. + 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://www.codebuddy.ai", + stateUrl: "https://www.codebuddy.ai/v2/plugin/auth/state", + tokenUrl: "https://www.codebuddy.ai/v2/plugin/auth/token", + refreshUrl: "https://www.codebuddy.ai/v2/plugin/auth/token/refresh", + userAgent: "IDE/2.63.2 CodeBuddy/2.63.2", + platform: "ide", + pollInterval: 5000, + }, + features: { + usage: true, + usageApikey: true, + }, +}; diff --git a/open-sse/providers/registry/index.js b/open-sse/providers/registry/index.js index cc1025a1..f083d79d 100644 --- a/open-sse/providers/registry/index.js +++ b/open-sse/providers/registry/index.js @@ -99,6 +99,11 @@ import p96 from "./xiaomi-mimo.js"; import p97 from "./xiaomi-tokenplan.js"; import p98 from "./youcom.js"; import p99 from "./alims-intl.js"; +import p100 from "./codebuddy-intl.js"; +import p101 from "./workbuddy.js"; +import p102 from "./trae.js"; +import p103 from "./zed.js"; +import p104 from "./windsurf.js"; export default [ p0, @@ -201,4 +206,9 @@ export default [ p97, p98, p99, + p100, + p101, + p102, + p103, + p104, ]; diff --git a/open-sse/providers/registry/qoder.js b/open-sse/providers/registry/qoder.js index 4ee2b52f..abcc873b 100644 --- a/open-sse/providers/registry/qoder.js +++ b/open-sse/providers/registry/qoder.js @@ -25,18 +25,22 @@ export default { }, }, models: [ - // { id: "auto", name: "Qoder Auto" }, - // { id: "ultimate", name: "Qoder Ultimate" }, - // { id: "performance", name: "Qoder Performance" }, - // { id: "efficient", name: "Qoder Efficient" }, - // { id: "lite", name: "Qoder Lite" }, - // { id: "qmodel", name: "Qwen 3.6 Plus (Qoder)" }, - { id: "qmodel_latest", name: "Qoder Qwen 3.7 Max" }, - // { id: "dmodel", name: "DeepSeek V4 Pro (Qoder)" }, - // { id: "dfmodel", name: "DeepSeek V4 Flash (Qoder)" }, - // { id: "gm51model", name: "GLM 5.1 (Qoder)" }, - // { id: "kmodel", name: "Kimi K2.6 (Qoder)" }, - // { id: "mmodel", name: "MiniMax M2.7 (Qoder)" }, + { id: "qoder-rome-30ba3b", name: "Qoder ROME" }, + { id: "glm-5.2", name: "GLM-5.2" }, + { id: "minimax-m3", name: "MiniMax M3" }, + { id: "qwen3-coder-plus", name: "Qwen3 Coder Plus" }, + { id: "qwen3-max", name: "Qwen3 Max" }, + { id: "qwen3-vl-plus", name: "Qwen3 Vision Plus" }, + { id: "kimi-k2-0905", name: "Kimi K2 0905" }, + { id: "qwen3-max-preview", name: "Qwen3 Max Preview" }, + { id: "kimi-k2", name: "Kimi K2" }, + { id: "deepseek-v3.2", name: "DeepSeek-V3.2-Exp" }, + { id: "deepseek-r1", name: "DeepSeek R1" }, + { id: "deepseek-v3", name: "DeepSeek V3" }, + { id: "qwen3-32b", name: "Qwen3 32B" }, + { id: "qwen3-235b-a22b-thinking-2507", name: "Qwen3 235B A22B Thinking 2507" }, + { id: "qwen3-235b-a22b-instruct", name: "Qwen3 235B A22B Instruct" }, + { id: "qwen3-235b", name: "Qwen3 235B" }, ], oauth: { openApiBaseUrl: "https://openapi.qoder.sh", diff --git a/open-sse/providers/registry/trae.js b/open-sse/providers/registry/trae.js new file mode 100644 index 00000000..d99ace61 --- /dev/null +++ b/open-sse/providers/registry/trae.js @@ -0,0 +1,77 @@ +// Trae (ByteDance marscode) provider registry entry. +// Auth + exchange URLs verified from cockpit-tools/src-tauri/src/modules/trae_oauth.rs. +// Region origins verified from trae_account.rs lines 63-66. +// Chat endpoint path /cloudide/api/v3/trae/Chat is GUESSED (TODO verify upstream). +export default { + id: "trae", + alias: "tr", + uiAlias: "tr", + aliases: ["marscode"], + category: "oauth", + authType: "oauth", + hasOAuth: true, + authModes: ["oauth"], + display: { + name: "Trae", + icon: "bolt", + color: "#FF6A00", + textIcon: "TR", + website: "https://www.trae.ai", + notice: { signupUrl: "https://www.trae.ai" }, + }, + transport: { + // IDE flow (cockpit-tools verified): x-cloudide-token auth, OpenAI-shaped SSE. + baseUrl: "https://api.marscode.com/cloudide/api/v3/trae/Chat", + format: "openai", + headers: { + "x-app-version": "3.5.54", + "x-app-type": "stable", + "x-env": "production", + "client_id": "ono9krqynydwx5", + "User-Agent": "Trae/1.0.0 antigravity-cockpit-tools", + }, + // Auth: x-cloudide-token + Authorization: Bearer — injected by executor buildHeaders. + auth: { + combined: true, + header: "x-cloudide-token", + scheme: "raw", + }, + usage: { + url: "https://api.marscode.com/cloudide/api/v3/trae/GetUserInfo", + }, + regions: { + cn: "https://api.marscode.com", + sg: "https://api.trae.ai", + us: "https://www.trae.ai", + }, + defaultRegion: "cn", + }, + oauth: { + clientId: "ono9krqynydwx5", + clientSecret: "-", + platform: "trae", + pollInterval: 1500, + // Login guidance returns LoginHost for browser open. + loginGuidanceUrl: "https://api.marscode.com/cloudide/api/v3/trae/GetLoginGuidance", + // ExchangeToken: refresh -> access (POST JSON, body below). + tokenUrl: "https://api.marscode.com/cloudide/api/v3/trae/oauth/ExchangeToken", + exchangeTokenUrl: "https://api.marscode.com/cloudide/api/v3/trae/oauth/ExchangeToken", + refreshUrl: "https://api.marscode.com/cloudide/api/v3/trae/oauth/ExchangeToken", + userInfoUrl: "https://api.marscode.com/cloudide/api/v3/trae/GetUserInfo", + // Trae refresh uses custom JSON body, not OAuth form — handled by refresh.js, not config-driven. + refresh: { encoding: "json" }, + }, + // Model catalog sourced from OmniRoute (IDE flow, core-normal.trae.ai). + models: [ + { id: "auto", name: "Auto (Server Picks)" }, + { id: "work", name: "Work (Fast)" }, + { id: "gemini-3.1-pro", name: "Gemini 3.1 Pro" }, + { id: "gemini-3-flash-solo", name: "Gemini 3 Flash" }, + { id: "minimax-m3", name: "MiniMax M3" }, + { id: "minimax-m2.7", name: "MiniMax M2.7" }, + { id: "kimi-k2.5", name: "Kimi K2.5" }, + { id: "gpt-5.4", name: "GPT 5.4" }, + { id: "gpt-5.2", name: "GPT 5.2" }, + ], + features: { usage: true }, +}; diff --git a/open-sse/providers/registry/windsurf.js b/open-sse/providers/registry/windsurf.js new file mode 100644 index 00000000..9d4222c8 --- /dev/null +++ b/open-sse/providers/registry/windsurf.js @@ -0,0 +1,146 @@ +// Windsurf provider registry — Firebase+Codeium+Devin auth chain. +// Chat transport is Codeium protobuf gRPC-Web: endpoint + schema are GUESS, +// the cockpit-tools source only documents auth/quota (SeatManagement) paths. +export default { + id: "windsurf", + alias: "ws", + uiAlias: "ws", + display: { + name: "Windsurf", + icon: "surfing", + color: "#14B8A6", + website: "https://windsurf.com", + notice: { signupUrl: "https://windsurf.com" }, + }, + category: "oauth", + authType: "oauth", + hasOAuth: true, + authModes: ["oauth", "apikey"], + + // TODO(chat): Codeium ServerService protobuf schema unknown — endpoint is a guess. + transport: { + // GUESS: Codeium chat lives under /exa.server_pb.ServerService/GetChatMessage. + baseUrl: "https://server.codeium.com/exa.server_pb.ServerService/GetChatMessage", + format: "windsurf", + headers: { + "Content-Type": "application/proto", + "Connect-Protocol-Version": "1", + "ideName": "Windsurf", + "extensionName": "codeium.windsurf", + }, + // Bearer of apiKey (sk-ws-... / Firebase-derived / Devin session) — Connect-Protocol scheme unverified. + auth: { combined: true, header: "Authorization" }, + }, + + // Auth chain (4 terminal paths, all yield apiKey): + // 1) OAuth web → Firebase JWT → POST register.windsurf.com/.../RegisterUser {firebase_id_token} → {apiKey, apiServerUrl, name} + // 2) sk-ws-... direct API key (apiKey used as metadata.apiKey on GetUserStatus) + // 3) Firebase JWT (eyJ...) → same RegisterUser exchange as #1 + // 4) Devin auth1_... → self-serve chain → ide_token used as apiKey on server.self-serve.windsurf.com + oauth: { + clientId: "3GUryQ7ldAeKEuD2obYnppsnmj58eP5u", + firebaseApiKey: "AIzaSyDsOl-1XpT5err0Tcn0TFFod1H8gVGIycY", + firebaseSignInUrl: "https://identitytoolkit.googleapis.com/v1/accounts:signInWithPassword", + registerUrl: "https://register.windsurf.com/exa.seat_management_pb.SeatManagementService/RegisterUser", + apiServerUrl: "https://server.codeium.com", + auth1ApiServerUrl: "https://server.self-serve.windsurf.com", + platform: "windsurf", + // Quota (Connect RPC, protobuf): POST windsurf.com/_backend/.../GetPlanStatus, + // headers Content-Type:application/proto + Connect-Protocol-Version:1 + X-Auth-Token:, + // body = field1:session_token, field2:varint 1. + quotaUrl: "https://windsurf.com/_backend/exa.seat_management_pb.SeatManagementService/GetPlanStatus", + }, + + // Catalog verified against model_configs_v2.bin from Devin CLI (2026.5.x). + // Source: OmniRoute registry (guanxiaol/WindsurfPoolAPI). Dot-notation ids; the + // executor MODEL_ALIAS_MAP would map these to Windsurf modelUid once proto chat + // is implemented. contextLength dropped — 9router schema uses id+name only. + models: [ + // Cognition / SWE + { id: "swe-1.6-fast", name: "SWE-1.6 Fast" }, + { id: "swe-1.6", name: "SWE-1.6" }, + { id: "swe-1.5-fast", name: "SWE-1.5 Fast" }, + { id: "swe-1.5", name: "SWE-1.5" }, + // Claude Opus 4.7 — effort-tiered + { id: "claude-opus-4.7-max", name: "Claude Opus 4.7 Max" }, + { id: "claude-opus-4.7-xhigh", name: "Claude Opus 4.7 XHigh" }, + { id: "claude-opus-4.7-high", name: "Claude Opus 4.7 High" }, + { id: "claude-opus-4.7-medium", name: "Claude Opus 4.7 Medium" }, + { id: "claude-opus-4.7-low", name: "Claude Opus 4.7 Low" }, + { id: "claude-opus-4.7-review", name: "Claude Opus 4.7 Review" }, + // Claude Sonnet/Opus 4.6 + { id: "claude-sonnet-4.6-thinking-1m", name: "Claude Sonnet 4.6 Thinking 1M" }, + { id: "claude-sonnet-4.6-1m", name: "Claude Sonnet 4.6 1M" }, + { id: "claude-sonnet-4.6-thinking", name: "Claude Sonnet 4.6 Thinking" }, + { id: "claude-sonnet-4.6", name: "Claude Sonnet 4.6" }, + { id: "claude-opus-4.6-thinking", name: "Claude Opus 4.6 Thinking" }, + { id: "claude-opus-4.6", name: "Claude Opus 4.6" }, + // Claude 4.5 + { id: "claude-opus-4.5-thinking", name: "Claude Opus 4.5 Thinking" }, + { id: "claude-opus-4.5", name: "Claude Opus 4.5" }, + { id: "claude-sonnet-4.5-thinking", name: "Claude Sonnet 4.5 Thinking" }, + { id: "claude-sonnet-4.5", name: "Claude Sonnet 4.5" }, + { id: "claude-haiku-4.5", name: "Claude Haiku 4.5" }, + // GPT-5.5 — effort-tiered + { id: "gpt-5.5-xhigh-fast", name: "GPT-5.5 XHigh Fast" }, + { id: "gpt-5.5-xhigh", name: "GPT-5.5 XHigh" }, + { id: "gpt-5.5-high-fast", name: "GPT-5.5 High Fast" }, + { id: "gpt-5.5-high", name: "GPT-5.5 High" }, + { id: "gpt-5.5-medium-fast", name: "GPT-5.5 Medium Fast" }, + { id: "gpt-5.5-medium", name: "GPT-5.5 Medium" }, + { id: "gpt-5.5-low-fast", name: "GPT-5.5 Low Fast" }, + { id: "gpt-5.5-low", name: "GPT-5.5 Low" }, + { id: "gpt-5.5-none-fast", name: "GPT-5.5 None Fast" }, + { id: "gpt-5.5-none", name: "GPT-5.5 None" }, + // GPT-5.4 — effort-tiered + { id: "gpt-5.4-xhigh-fast", name: "GPT-5.4 XHigh Fast" }, + { id: "gpt-5.4-xhigh", name: "GPT-5.4 XHigh" }, + { id: "gpt-5.4-high-fast", name: "GPT-5.4 High Fast" }, + { id: "gpt-5.4-high", name: "GPT-5.4 High" }, + { id: "gpt-5.4-medium-fast", name: "GPT-5.4 Medium Fast" }, + { id: "gpt-5.4-medium", name: "GPT-5.4 Medium" }, + { id: "gpt-5.4-low-fast", name: "GPT-5.4 Low Fast" }, + { id: "gpt-5.4-low", name: "GPT-5.4 Low" }, + { id: "gpt-5.4-none-fast", name: "GPT-5.4 None Fast" }, + { id: "gpt-5.4-none", name: "GPT-5.4 None" }, + { id: "gpt-5.4-mini-xhigh", name: "GPT-5.4 Mini XHigh" }, + { id: "gpt-5.4-mini-high", name: "GPT-5.4 Mini High" }, + { id: "gpt-5.4-mini-medium", name: "GPT-5.4 Mini Medium" }, + { id: "gpt-5.4-mini-low", name: "GPT-5.4 Mini Low" }, + // GPT-5.3 Codex + { id: "gpt-5.3-codex-xhigh-fast", name: "GPT-5.3 Codex XHigh Fast" }, + { id: "gpt-5.3-codex-xhigh", name: "GPT-5.3 Codex XHigh" }, + { id: "gpt-5.3-codex-high-fast", name: "GPT-5.3 Codex High Fast" }, + { id: "gpt-5.3-codex-high", name: "GPT-5.3 Codex High" }, + { id: "gpt-5.3-codex-medium-fast", name: "GPT-5.3 Codex Medium Fast" }, + { id: "gpt-5.3-codex-medium", name: "GPT-5.3 Codex Medium" }, + { id: "gpt-5.3-codex-low-fast", name: "GPT-5.3 Codex Low Fast" }, + { id: "gpt-5.3-codex-low", name: "GPT-5.3 Codex Low" }, + // GPT-5.2 / 5 + { id: "gpt-5.2-xhigh", name: "GPT-5.2 XHigh" }, + { id: "gpt-5.2-high", name: "GPT-5.2 High" }, + { id: "gpt-5.2-medium", name: "GPT-5.2 Medium" }, + { id: "gpt-5.2-low", name: "GPT-5.2 Low" }, + { id: "gpt-5.2-none", name: "GPT-5.2 None" }, + { id: "gpt-5", name: "GPT-5" }, + // GPT-4.1 / 4o + { id: "gpt-4.1", name: "GPT-4.1" }, + { id: "gpt-4.1-mini", name: "GPT-4.1 Mini" }, + { id: "gpt-4.1-nano", name: "GPT-4.1 Nano" }, + { id: "gpt-4o", name: "GPT-4o" }, + { id: "gpt-4o-mini", name: "GPT-4o Mini" }, + // Gemini + { id: "gemini-3.1-pro-high", name: "Gemini 3.1 Pro High" }, + { id: "gemini-3.1-pro-low", name: "Gemini 3.1 Pro Low" }, + { id: "gemini-3.0-flash-high", name: "Gemini 3 Flash High" }, + { id: "gemini-3.0-flash-medium", name: "Gemini 3 Flash Medium" }, + { id: "gemini-3.0-flash-low", name: "Gemini 3 Flash Low" }, + { id: "gemini-3.0-flash-minimal", name: "Gemini 3 Flash Minimal" }, + { id: "gemini-2.5-pro", name: "Gemini 2.5 Pro" }, + // Others + { id: "deepseek-v4", name: "DeepSeek V4" }, + { id: "kimi-k2.6", name: "Kimi K2.6" }, + { id: "kimi-k2.5", name: "Kimi K2.5" }, + { id: "glm-5.1", name: "GLM-5.1" }, + ], +}; diff --git a/open-sse/providers/registry/workbuddy.js b/open-sse/providers/registry/workbuddy.js new file mode 100644 index 00000000..8adffe7f --- /dev/null +++ b/open-sse/providers/registry/workbuddy.js @@ -0,0 +1,73 @@ +export default { + id: "workbuddy", + // Short model prefix (wb/glm-5.2). WorkBuddy is a B2B/enterprise skin of + // CodeBuddy CN (same codebuddy.cn backend), so models mirror codebuddy-cn. + alias: "wb", + uiAlias: "wb", + hidden: false, + priority: 90, + display: { + name: "WorkBuddy", + icon: "smart_toy", + color: "#006EFF", + website: "https://www.codebuddy.cn", + notice: { + signupUrl: "https://www.codebuddy.cn", + }, + }, + category: "oauth", + authModes: ["oauth", "apikey"], + hasOAuth: true, + transport: { + // Same OpenAI-compatible gateway as codebuddy-cn; platform=workbuddy is + // distinguished at the OAuth layer, not the chat endpoint. + baseUrl: "https://www.codebuddy.cn/v2/chat/completions", + forceStream: true, + 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: { + // Same codebuddy.cn host as codebuddy-cn; only platform param differs + // (workbuddy vs CLI). Prefix /v2/plugin matches cockpit-tools Rust. + baseUrl: "https://www.codebuddy.cn", + stateUrl: "https://www.codebuddy.cn/v2/plugin/auth/state", + tokenUrl: "https://www.codebuddy.cn/v2/plugin/auth/token", + refreshUrl: "https://www.codebuddy.cn/v2/plugin/auth/token/refresh", + userAgent: "CLI/2.63.2 CodeBuddy/2.63.2", + platform: "workbuddy", + pollInterval: 5000, + }, + features: { + usage: true, + usageApikey: true, + }, +}; diff --git a/open-sse/providers/registry/zed.js b/open-sse/providers/registry/zed.js new file mode 100644 index 00000000..b7476525 --- /dev/null +++ b/open-sse/providers/registry/zed.js @@ -0,0 +1,72 @@ +// Zed provider — RSA keypair callback auth (NOT standard OAuth). +// Source of truth: .repo/cockpit-tools/src-tauri/src/modules/zed_oauth.rs + zed_account.rs. +export default { + id: "zed", + priority: 10, + alias: "zd", + uiAlias: "zd", + display: { + name: "Zed", + icon: "code", + color: "#A855F7", + website: "https://zed.dev", + notice: { + signupUrl: "https://zed.dev/native_app_signin", + }, + }, + category: "oauth", + authType: "oauth", + hasOAuth: true, + + transport: { + // Zed hosted LLM aggregator (OmniRoute-verified): cloud.zed.dev/completions is a + // multi-format proxy fronting Anthropic/OpenAI/Google/xAI depending on the model. + // Wire protocol = NDJSON/SSE-ish stream authenticated with a short-lived LLM bearer + // token exchanged from the RSA-decrypted access_token (see open-sse/shared/zedAuth + // in OmniRoute). cockpit-tools only covered the RSA login + cloud.zed.dev quota path. + baseUrl: "https://cloud.zed.dev/completions", + format: "openai", + forceStream: true, + headers: { + "content-type": "application/json", + }, + // Auth scheme is non-standard: "Authorization: " plus a duplicate + // x-zed-cloud-token header (verified in zed_account.rs build_authorization_header + + // cloud fetch). Executor builds both; scheme here is a marker for config-driven tooling. + auth: { + combined: true, + header: "Authorization", + scheme: " ", // placeholder — real value built in executor + }, + usage: { + url: "https://cloud.zed.dev/client/users/me", // verified in zed_account.rs + }, + // Live catalog discovery — Zed's hosted model list changes frequently and is fetched + // per-connection rather than hardcoded (OmniRoute pattern). + modelsUrl: "https://cloud.zed.dev/models", + }, + + // Empty static catalog + passthrough: Zed fronts a rotating set of upstream models + // (Claude/GPT/Gemini/Grok). Resolved live via modelsUrl; any client-sent model id is + // forwarded as-is rather than validated against a frozen list. + models: [], + passthroughModels: true, + + oauth: { + // Zed auth flow is RSA-based, NOT OAuth2/PKCE: + // 1. App generates RSA-2048 keypair locally (PKCS#1 DER, URL-safe base64). + // 2. Bind random TCP port on 127.0.0.1. + // 3. Open https://zed.dev/native_app_signin?native_app_port={port}&native_app_public_key={pub}. + // 4. After login, browser redirects http://127.0.0.1:{port}/?user_id=...&access_token=... + // where access_token = base64(RSA-encrypted plaintext token). + // 5. Decrypt with private key (OAEP-SHA256, fallback PKCS1v15). Store user_id + plaintext token. + // No clientId/clientSecret/tokenUrl/refreshUrl — long-lived access_token, no refresh. + authorizeUrl: "https://zed.dev/native_app_signin", + platform: "zed", + rsaKeyExchange: true, // new flag: signals frontend/router this flow needs local RSA + TCP listener. + }, + + features: { + usage: true, + }, +}; diff --git a/open-sse/services/tokenRefresh.js b/open-sse/services/tokenRefresh.js index 55f4582e..c8cced77 100644 --- a/open-sse/services/tokenRefresh.js +++ b/open-sse/services/tokenRefresh.js @@ -13,6 +13,11 @@ import { refreshGitHubToken, refreshCopilotToken, refreshCodebuddyToken, + refreshCodebuddyIntlToken, + refreshWorkbuddyToken, + refreshTraeToken, + refreshZedToken, + refreshWindsurfToken, classifyOAuthRefreshError, } from "./tokenRefresh/providers.js"; @@ -29,6 +34,11 @@ export { refreshGitHubToken, refreshCopilotToken, refreshCodebuddyToken, + refreshCodebuddyIntlToken, + refreshWorkbuddyToken, + refreshTraeToken, + refreshZedToken, + refreshWindsurfToken, classifyOAuthRefreshError, }; @@ -138,6 +148,11 @@ const REFRESH_HANDLERS = { "grok-cli": (c, log) => refreshXaiToken(c.refreshToken, log), gcli: (c, log) => refreshXaiToken(c.refreshToken, log), "codebuddy-cn": (c, log) => refreshCodebuddyToken(c.refreshToken, log), + "codebuddy-intl": (c, log) => refreshCodebuddyIntlToken(c.refreshToken, log), + workbuddy: (c, log) => refreshWorkbuddyToken(c.refreshToken, log), + trae: (c, log) => refreshTraeToken(c.refreshToken, c, log), + zed: () => refreshZedToken(), + windsurf: (c, log) => refreshWindsurfToken(c, log), // Kimi Code OAuth (merged into id `kimi`); legacy id still routes here kimi: (c, log) => refreshKimiToken(c.refreshToken, c, log), "kimi-coding": (c, log) => refreshKimiToken(c.refreshToken, c, log), diff --git a/open-sse/services/tokenRefresh/providers.js b/open-sse/services/tokenRefresh/providers.js index 3fc8c4b2..d82bfdeb 100644 --- a/open-sse/services/tokenRefresh/providers.js +++ b/open-sse/services/tokenRefresh/providers.js @@ -668,3 +668,198 @@ export async function refreshCodebuddyToken(refreshToken, log) { }; }, log); } + +export async function refreshCodebuddyIntlToken(refreshToken, log) { + if (!refreshToken) return null; + return dedupRefresh("codebuddy-intl", refreshToken, async () => { + const oauth = PROVIDER_OAUTH["codebuddy-intl"] || {}; + 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": "www.codebuddy.ai", + "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 intl 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 intl token refresh returned no token", { + code: data.code, + msg: data.msg, + }); + return null; + } + + log?.info?.("TOKEN_REFRESH", "Successfully refreshed CodeBuddy intl 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); +} + +export async function refreshWorkbuddyToken(refreshToken, log) { + if (!refreshToken) return null; + return dedupRefresh("workbuddy", refreshToken, async () => { + const oauth = PROVIDER_OAUTH["workbuddy"] || {}; + 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": "www.codebuddy.cn", + "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 WorkBuddy token", { + status: response.status, + error: errorText, + }); + return null; + } + + const data = await response.json(); + if (data.code !== 0 || !data.data?.accessToken) { + log?.error?.("TOKEN_REFRESH", "WorkBuddy token refresh returned no token", { + code: data.code, + msg: data.msg, + }); + return null; + } + + log?.info?.("TOKEN_REFRESH", "Successfully refreshed WorkBuddy 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); +} + +// Trae refresh — POST ExchangeToken with JSON body {ClientID, RefreshToken, ClientSecret, UserID}. +// Response: {Result: {AccessToken, RefreshToken, TokenType, ExpiresAt}}. +// Source: cockpit-tools/src-tauri/src/modules/trae_oauth.rs (TRAE_EXCHANGE_TOKEN_PATH). +export async function refreshTraeToken(refreshToken, credentials, log) { + if (!refreshToken) return null; + const oauth = PROVIDER_OAUTH.trae || {}; + const url = oauth.exchangeTokenUrl || oauth.tokenUrl; + if (!url) { + log?.warn?.("TOKEN_REFRESH", "No Trae exchangeTokenUrl configured"); + return null; + } + + return dedupRefresh("trae", refreshToken, async () => { + try { + const response = await fetch(url, { + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + "User-Agent": "Trae/1.0.0 antigravity-cockpit-tools", + }, + body: JSON.stringify({ + ClientID: oauth.clientId || "ono9krqynydwx5", + RefreshToken: refreshToken, + ClientSecret: oauth.clientSecret || "-", + UserID: "", + }), + }); + + if (!response.ok) { + const errorText = await response.text(); + log?.error?.("TOKEN_REFRESH", "Failed to refresh Trae token", { + status: response.status, + error: errorText, + }); + return null; + } + + const payload = await response.json(); + const result = payload?.Result || payload?.result || payload; + const accessToken = result?.AccessToken || result?.accessToken; + if (!accessToken) { + log?.error?.("TOKEN_REFRESH", "Trae refresh returned no AccessToken", { payload }); + return null; + } + + const newRefresh = result?.RefreshToken || result?.refreshToken || refreshToken; + const expiresAt = result?.ExpiresAt || result?.expiresAt; + let expiresIn; + if (typeof expiresAt === "number") { + expiresIn = Math.max(1, expiresAt - Math.floor(Date.now() / 1000)); + } else if (typeof expiresAt === "string") { + const ms = new Date(expiresAt).getTime() - Date.now(); + expiresIn = ms > 0 ? Math.floor(ms / 1000) : undefined; + } + + log?.info?.("TOKEN_REFRESH", "Successfully refreshed Trae token", { + hasNewAccessToken: !!accessToken, + hasNewRefreshToken: newRefresh !== refreshToken, + expiresIn, + }); + + return { + accessToken, + refreshToken: newRefresh, + expiresIn, + }; + } catch (error) { + log?.error?.("TOKEN_REFRESH", `Error refreshing Trae token: ${error.message}`); + return null; + } + }, log); +} + +// Zed access_token is long-lived; auth flow returns no refresh_token. +// No refresh possible — re-login required when token expires/revoked. +// Mirrors cursor/kilocode null-refresh pattern. +export function refreshZedToken() { + return null; +} + +// Windsurf apiKey is the long-lived terminal credential (no OAuth2 refresh_token +// grant yields a fresh apiKey). Refresh handled out-of-band by the caller. +// TODO(firebase): if short-lived Firebase JWT credentials must be refreshed, +// re-run RegisterUser with the refreshed Firebase JWT (separate code path). +export async function refreshWindsurfToken(credentials, log) { + log?.info?.( + "TOKEN_REFRESH", + "windsurf: apiKey is long-lived (no refresh_token flow) — skipping" + ); + return null; +} diff --git a/open-sse/shared/zedAuth.js b/open-sse/shared/zedAuth.js new file mode 100644 index 00000000..5f38298b --- /dev/null +++ b/open-sse/shared/zedAuth.js @@ -0,0 +1,416 @@ +// Zed hosted LLM aggregator — auth + model-catalog helpers. +// Ported from OmniRoute open-sse/shared/zedAuth.ts (plain JS, no TS types). +// +// Zed's cloud (cloud.zed.dev) authenticates native apps with a self-generated RSA +// keypair instead of a registered OAuth client_id/secret: +// 1. Client generates an ephemeral RSA keypair. +// 2. Sends the public key to zed.dev/native_app_signin. +// 3. User signs in via browser; Zed redirects to a local callback with the +// access token RSA-encrypted against the public key. +// 4. Client decrypts locally with the private key that never left the host. +// No embedded client_id/secret — the credential is a per-login keypair. + +import crypto from "node:crypto"; +import { proxyAwareFetch } from "../utils/proxyFetch.js"; + +export const ZED_WEB_BASE_URL = "https://zed.dev"; +export const ZED_CLOUD_BASE_URL = "https://cloud.zed.dev"; +export const ZED_LLM_BASE_URL = "https://cloud.zed.dev"; + +export const ZED_HEADERS = { + expiredToken: "x-zed-expired-token", + outdatedToken: "x-zed-outdated-token", + clientSupportsStatus: "x-zed-client-supports-status-messages", + clientSupportsStreamEnded: + "x-zed-client-supports-stream-ended-request-completion-status", + serverSupportsStatus: "x-zed-server-supports-status-messages", + clientSupportsXai: "x-zed-client-supports-x-ai", + systemId: "x-zed-system-id", +}; + +const PRIVATE_KEY_PREFIX = "zed-rsa-pkcs1:"; +const LLM_TOKEN_TTL_MS = 50 * 60 * 1000; +const MODEL_CACHE_TTL_MS = 60 * 60 * 1000; + +const llmTokenCache = new Map(); +const modelCache = new Map(); +const modelInflight = new Map(); + +function b64url(value) { + return Buffer.from(value).toString("base64url"); +} + +function b64urlPadded(buf) { + return buf.toString("base64").replace(/\+/g, "-").replace(/\//g, "_"); +} + +function fromB64url(value) { + return Buffer.from(String(value || ""), "base64url").toString("utf8"); +} + +function normalizeBaseUrl(baseUrl, fallback) { + return String(baseUrl || fallback).replace(/\/+$/, ""); +} + +function zedUrl(config, key, path, fallbackBase) { + const base = normalizeBaseUrl(config?.[key], fallbackBase); + return `${base}${path}`; +} + +/** Encode a PEM private key as an opaque verifier (flows through the OAuth codeVerifier slot). */ +export function encodeZedPrivateKeyVerifier(privateKeyPem) { + return `${PRIVATE_KEY_PREFIX}${b64url(privateKeyPem)}`; +} + +export function decodeZedPrivateKeyVerifier(verifier) { + const value = String(verifier || ""); + if (!value.startsWith(PRIVATE_KEY_PREFIX)) { + throw new Error("Missing Zed private key verifier; restart the login flow"); + } + return fromB64url(value.slice(PRIVATE_KEY_PREFIX.length)); +} + +/** Generate a fresh RSA keypair + the zed.dev native_app_signin URL for it. */ +export function createZedNativeAuthData(config = {}, options = {}) { + const { publicKey, privateKey } = crypto.generateKeyPairSync("rsa", { + modulusLength: 2048, + publicKeyEncoding: { type: "pkcs1", format: "der" }, + privateKeyEncoding: { type: "pkcs1", format: "pem" }, + }); + + const nativeAppPort = Number( + options.nativeAppPort || config.defaultNativeAppPort || 58443, + ); + const systemId = options.systemId || crypto.randomUUID(); + const publicKeyString = b64urlPadded(publicKey); + const signInUrl = new URL( + `${normalizeBaseUrl(config.webBaseUrl, ZED_WEB_BASE_URL)}/native_app_signin`, + ); + signInUrl.searchParams.set("native_app_port", String(nativeAppPort)); + signInUrl.searchParams.set("native_app_public_key", publicKeyString); + if (systemId) signInUrl.searchParams.set("system_id", systemId); + + return { + authUrl: signInUrl.toString(), + privateKeyVerifier: encodeZedPrivateKeyVerifier(privateKey), + nativeAppPort, + systemId, + publicKey: publicKeyString, + }; +} + +/** Parse the pasted native-app callback URL/JSON/query into userId + encrypted token. */ +export function parseZedCallbackPayload(input) { + const raw = String(input || "").trim(); + if (!raw) throw new Error("Missing Zed callback URL"); + + let data = {}; + try { + data = JSON.parse(raw); + } catch { + let url; + try { + url = new URL(raw); + } catch { + try { + url = new URL(`http://127.0.0.1/?${raw.replace(/^\?/, "")}`); + } catch { + throw new Error("Invalid Zed callback URL"); + } + } + url.searchParams.forEach((value, key) => { + data[key] = value; + }); + } + + const userId = data.user_id || data.userId; + const encryptedAccessToken = data.access_token || data.accessToken || data.token; + if (!userId || !encryptedAccessToken) { + throw new Error("Zed callback must include user_id and access_token"); + } + return { userId: String(userId), encryptedAccessToken: String(encryptedAccessToken) }; +} + +/** Decrypt the RSA-encrypted access token using the stored private key. */ +export function decryptZedAccessToken(encryptedAccessToken, privateKeyVerifier) { + const privateKey = decodeZedPrivateKeyVerifier(privateKeyVerifier); + const encrypted = Buffer.from(String(encryptedAccessToken), "base64url"); + try { + return crypto + .privateDecrypt( + { key: privateKey, padding: crypto.constants.RSA_PKCS1_OAEP_PADDING, oaepHash: "sha256" }, + encrypted, + ) + .toString("utf8"); + } catch (oaepError) { + try { + return crypto + .privateDecrypt( + { key: privateKey, padding: crypto.constants.RSA_PKCS1_PADDING }, + encrypted, + ) + .toString("utf8"); + } catch { + const message = oaepError instanceof Error ? oaepError.message : String(oaepError); + throw new Error(`Failed to decrypt Zed access token: ${message}`); + } + } +} + +export function buildZedUserAuthHeader(credentials) { + const psd = credentials?.providerSpecificData || {}; + const userId = psd.userId || credentials?.userId; + const accessToken = credentials?.accessToken || credentials?.apiKey; + if (!userId || !accessToken) { + throw new Error("Zed credential is missing userId or accessToken"); + } + return `${userId} ${accessToken}`; +} + +function getSystemId(credentials) { + return String( + credentials?.providerSpecificData?.systemId || credentials?.systemId || "", + ); +} + +async function fetchJson(url, options) { + const res = await proxyAwareFetch(url, options); + const text = await res.text(); + let data = null; + if (text) { + try { + data = JSON.parse(text); + } catch { + data = { raw: text }; + } + } + if (!res.ok) { + const message = + data?.message || data?.error?.message || data?.error || text || `HTTP ${res.status}`; + const err = new Error(String(message)); + err.status = res.status; + err.body = data; + throw err; + } + return data; +} + +export async function fetchZedAuthenticatedUser(credentials, options = {}) { + const config = options.config || {}; + const headers = { + Accept: "application/json", + Authorization: buildZedUserAuthHeader(credentials), + }; + const systemId = getSystemId(credentials); + if (systemId) headers[ZED_HEADERS.systemId] = systemId; + + return fetchJson(zedUrl(config, "cloudBaseUrl", "/client/users/me", ZED_CLOUD_BASE_URL), { + method: "GET", + headers, + signal: options.signal ?? undefined, + }); +} + +function normalizeOrganizationId(value) { + if (!value) return ""; + if (typeof value === "string") return value; + if (typeof value === "object" && value !== null) { + if (typeof value[0] === "string") return value[0]; + if (typeof value.id === "string") return value.id; + } + return String(value); +} + +export function resolveZedOrganizationId(credentials, userInfo = null) { + const psd = credentials?.providerSpecificData || {}; + const explicit = normalizeOrganizationId(psd.organizationId || psd.defaultOrganizationId); + if (explicit) return explicit; + const fromUser = normalizeOrganizationId( + userInfo?.default_organization_id || userInfo?.defaultOrganizationId, + ); + if (fromUser) return fromUser; + const orgs = userInfo?.organizations || []; + const org = orgs.find((item) => item?.is_personal) || orgs[0]; + return normalizeOrganizationId(org?.id); +} + +function zedUserCacheKey(credentials, organizationId) { + const psd = credentials?.providerSpecificData || {}; + const userId = psd.userId || credentials?.userId || "unknown"; + const token = credentials?.accessToken || credentials?.apiKey || ""; + return `${userId}:${organizationId || "default"}:${token.slice(-16)}`; +} + +function zedModelCacheKey(credentials) { + const psd = credentials?.providerSpecificData || {}; + const org = psd.organizationId || psd.defaultOrganizationId || "default"; + const token = credentials?.accessToken || credentials?.apiKey || ""; + return `${psd.userId || "unknown"}:${org}:${token.slice(-16)}`; +} + +export async function fetchZedLlmToken(credentials, options = {}) { + const config = options.config || {}; + let organizationId = options.organizationId || resolveZedOrganizationId(credentials); + if (!organizationId) { + const userInfo = await fetchZedAuthenticatedUser(credentials, options); + organizationId = resolveZedOrganizationId(credentials, userInfo); + } + if (!organizationId) throw new Error("No Zed organization selected"); + + const cacheKey = zedUserCacheKey(credentials, organizationId); + const cached = llmTokenCache.get(cacheKey); + if (!options.forceRefresh && cached && cached.expiresAt > Date.now()) return cached.token; + + const headers = { + "Content-Type": "application/json", + Accept: "application/json", + Authorization: buildZedUserAuthHeader(credentials), + }; + const systemId = getSystemId(credentials); + if (systemId) headers[ZED_HEADERS.systemId] = systemId; + + const data = await fetchJson( + zedUrl(config, "cloudBaseUrl", "/client/llm_tokens", ZED_CLOUD_BASE_URL), + { + method: "POST", + headers, + body: JSON.stringify({ organization_id: organizationId }), + signal: options.signal ?? undefined, + }, + ); + const token = + typeof data?.token === "string" ? data.token : data?.token?.[0] || data?.token?.value; + if (!token) throw new Error("Zed did not return an LLM token"); + llmTokenCache.set(cacheKey, { token, expiresAt: Date.now() + LLM_TOKEN_TTL_MS }); + return token; +} + +export function shouldRefreshZedLlmToken(response) { + return ( + response?.status === 401 || + !!response?.headers?.has?.(ZED_HEADERS.expiredToken) || + !!response?.headers?.has?.(ZED_HEADERS.outdatedToken) + ); +} + +export async function zedLlmFetch(credentials, path, options = {}) { + const config = options.config || {}; + const url = zedUrl(config, "llmBaseUrl", path, ZED_LLM_BASE_URL); + const buildRequest = async (forceRefresh) => { + const token = await fetchZedLlmToken(credentials, { ...options, forceRefresh }); + return proxyAwareFetch(url, { + ...options.fetchOptions, + headers: { + ...(options.fetchOptions?.headers || {}), + Authorization: `Bearer ${token}`, + }, + signal: options.signal ?? undefined, + }); + }; + + let response = await buildRequest(false); + if (shouldRefreshZedLlmToken(response)) { + response = await buildRequest(true); + } + return response; +} + +function normalizeZedModelId(id) { + if (!id) return ""; + if (typeof id === "string") return id; + if (typeof id === "object" && id !== null) { + if (typeof id[0] === "string") return id[0]; + if (typeof id.id === "string") return id.id; + } + return String(id); +} + +export function mapZedModel(model) { + const id = normalizeZedModelId(model?.id); + if (!id) return null; + return { + id, + name: model.display_name || model.displayName || id, + provider: model.provider, + isLatest: !!model.is_latest, + contextLength: model.max_token_count ?? model.maxTokenCount, + contextLengthInMaxMode: model.max_token_count_in_max_mode ?? model.maxTokenCountInMaxMode, + maxOutputTokens: model.max_output_tokens ?? model.maxOutputTokens, + supportsTools: !!model.supports_tools, + supportsImages: !!model.supports_images, + supportsThinking: !!model.supports_thinking, + supportsDisablingThinking: !!model.supports_disabling_thinking, + supportsFastMode: !!model.supports_fast_mode, + supportsServerSideCompaction: !!model.supports_server_side_compaction, + supportedEffortLevels: model.supported_effort_levels ?? model.supportedEffortLevels ?? [], + supportsStreamingTools: !!model.supports_streaming_tools, + supportsParallelToolCalls: !!model.supports_parallel_tool_calls, + isDisabled: !!model.is_disabled, + disabledReason: model.disabled_reason ?? null, + }; +} + +/** Resolve (and cache) the live Zed model catalog. Never hardcoded — always a live fetch. */ +export async function resolveZedModels(credentials, options = {}) { + if (!credentials?.accessToken) return null; + const key = zedModelCacheKey(credentials); + const cached = modelCache.get(key); + if (!options.forceRefresh && cached && cached.expiresAt > Date.now()) return cached; + + const existing = modelInflight.get(key); + if (existing && !options.forceRefresh) return existing; + + const promise = (async () => { + const response = await zedLlmFetch(credentials, "/models", { + ...options, + fetchOptions: { + method: "GET", + headers: { + Accept: "application/json", + [ZED_HEADERS.clientSupportsXai]: "true", + }, + }, + }); + if (!response.ok) { + const text = await response.text().catch(() => ""); + throw new Error(`Zed models failed: ${response.status} ${text}`); + } + const data = await response.json(); + const rawModels = Array.isArray(data?.models) ? data.models : []; + const models = rawModels + .map(mapZedModel) + .filter(Boolean) + .filter((model) => !model.isDisabled); + const rawById = new Map(); + for (const raw of rawModels) { + const id = normalizeZedModelId(raw?.id); + if (id) rawById.set(id, raw); + } + const entry = { + expiresAt: Date.now() + MODEL_CACHE_TTL_MS, + models, + rawModels, + rawById, + defaultModel: normalizeZedModelId(data?.default_model ?? data?.defaultModel), + defaultFastModel: normalizeZedModelId(data?.default_fast_model ?? data?.defaultFastModel), + recommendedModels: (data?.recommended_models || data?.recommendedModels || []) + .map(normalizeZedModelId) + .filter(Boolean), + }; + modelCache.set(key, entry); + return entry; + })(); + + modelInflight.set(key, promise); + try { + return await promise; + } finally { + if (modelInflight.get(key) === promise) modelInflight.delete(key); + } +} + +export function clearZedCaches() { + llmTokenCache.clear(); + modelCache.clear(); + modelInflight.clear(); +} diff --git a/public/providers/codebuddy-intl.png b/public/providers/codebuddy-intl.png new file mode 100644 index 0000000000000000000000000000000000000000..c836282f29512e354728c001c7f49086f401af0a GIT binary patch literal 17688 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 XY#OJ9MLDRA00000NkvXXu0mjfL3md! literal 0 HcmV?d00001 diff --git a/public/providers/trae.png b/public/providers/trae.png new file mode 100644 index 0000000000000000000000000000000000000000..c056daf0a3d813af9020f23ee09bd0515bd91bcb GIT binary patch literal 2328 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

YVtCWuO@ zUgC_W>o7$})*B>wy>6-{c#fRGv{rlWyrBI-qvVI;$NM4T$JF6Twgm6#c?-DtnEqCj zLP8JE*6ZNI*fRxinMm9`kwKA%%v+~CS#gcK0bD8rn=ER7?Qbb;? z(;O+CJ4{!Qk!Wmm#4<}=4$b>Ue^B6=n7W7fDsN~@v9lpb#OxAcB9Vb|9m8?+MMro<2E~T?)S5&W?7{6iUU|t-ngVuS) z76{5-n*9W`pazXK5T3v7T##{~r&(8qM0oyE4X{}G_e+cDyUrha-F-LP6Y zeDLi=(eR%f=<-QN*sU&+ocFfl;IMv>gGlEaiB_zODlLnV$&7hDy^1Y*_-zoYih147 zm}ncrd#c}}R+h=_P@#2Q?2+&7xIuo7l0=g?5~Qi~{I&P@%4K-i9%1qCk3a%I}B z0Fj^kj|8|!m>Q+Hldnr-e08|458DZk8C$qAuyw+=$rQm|XMUC?-(PP05d5egv$}~> z**t@A1s5bV{B$y3pJ4w?$&@x=WO$*Wr}ePsPsBK2{mhuZzLLLy9x#JJ`v=OKjZi{y~tL}#p)%FPqk_!e)Qvw zUIkt87O%GpnfDmgHvv%DHX|GlP70M* zxn5nE%KM?h&)O*Svg{SnY94T2!jr2fq#Wps@&gUQvQ?FQd$>|#4ME!KCHmG&P~5Zj zs_pw$zt2sYrjhw;}gZe38n zo~SJl%!3n;m6x*h#*f)CK{EC^tSGN;2MW0-fZ{Bk5OD%>l>W7ZanmB2Ywr5R;7gq@ z<^BwW{~(oZRDbPDSS=wqY}F_*d``1V#}-E*e(aXS?tU52^Ym!;jsY=@##}`tOL|ujjrC|D^^wW7a=o;bj+Qvz zrdmQTZ>3+hkYst5C|;e`qF?vLl9o8%_ zO$8tt`D#cY99GoOU%b_s?_4!4V<^oq3=#Tn^Vg@ z@e(TsvlB1>nYtNa{z;FX)mMjtN_{Pp5tQ{Ky=K9Y(VYdY&=+2WQex^aadEO&I^uUh9AndvCpAF$Tg*{~V}BRXQ0Btbbk!1MaJihX2Y)JNo6# z*za0??6p^3$Kwlf9w=j{eW2rY?b~*Rmxf$kOY<40TbDPSmu=Xs>1uoD5+(arN(~qt zm)rYMk#M@g4Vg%}CApnVVx+1yzVC*u>Y^{!5GQe`xYh1FA#fpYg)_qE1HT{Gp3!&9 z(}qPIcX!ye?3lveCUi~Jm)y81p*4n=Hn>vC6)Uv2?S8uLU}u{ZC;UX%EzpieCz>^^ zy?rlsVT}&o+w0b*CWhs$X!mY|wxC!Nef#s`URS75JfElHJ)g$k%sA$HFhTx7DVj3C z4||V))c1~RV`Mj}UZkq-=2EHdi+K?ET<%9yuv-X&_nCE#&)^F-5M78@;5ns4At`y& w8>i0lni=`3q(xiLyz0LgbN@F$&uXk(xOVrLIa+vG{*Cu&Z6mEZO^4|J0j36OUH||9 literal 0 HcmV?d00001 diff --git a/public/providers/workbuddy.png b/public/providers/workbuddy.png new file mode 100644 index 0000000000000000000000000000000000000000..c836282f29512e354728c001c7f49086f401af0a GIT binary patch literal 17688 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 XY#OJ9MLDRA00000NkvXXu0mjfL3md! literal 0 HcmV?d00001 diff --git a/public/providers/zed.png b/public/providers/zed.png new file mode 100644 index 0000000000000000000000000000000000000000..009c3e9d1152a9a363969146105afe88fbe07337 GIT binary patch literal 21456 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 literal 0 HcmV?d00001