feat(providers): add trae/windsurf/zed/workbuddy/codebuddy-intl + icons
- 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 <noreply@anthropic.com>
This commit is contained in:
30
open-sse/executors/codebuddy-intl.js
Normal file
30
open-sse/executors/codebuddy-intl.js
Normal file
@@ -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;
|
||||
@@ -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";
|
||||
|
||||
22
open-sse/executors/trae.js
Normal file
22
open-sse/executors/trae.js
Normal file
@@ -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.
|
||||
}
|
||||
42
open-sse/executors/windsurf.js
Normal file
42
open-sse/executors/windsurf.js
Normal file
@@ -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;
|
||||
31
open-sse/executors/workbuddy.js
Normal file
31
open-sse/executors/workbuddy.js
Normal file
@@ -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;
|
||||
305
open-sse/executors/zed.js
Normal file
305
open-sse/executors/zed.js
Normal file
@@ -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": <provider-shaped-chunk>}` / `{"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 <zed-llm-token>" },
|
||||
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;
|
||||
74
open-sse/providers/registry/codebuddy-intl.js
Normal file
74
open-sse/providers/registry/codebuddy-intl.js
Normal file
@@ -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,
|
||||
},
|
||||
};
|
||||
@@ -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,
|
||||
];
|
||||
|
||||
@@ -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",
|
||||
|
||||
77
open-sse/providers/registry/trae.js
Normal file
77
open-sse/providers/registry/trae.js
Normal file
@@ -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 },
|
||||
};
|
||||
146
open-sse/providers/registry/windsurf.js
Normal file
146
open-sse/providers/registry/windsurf.js
Normal file
@@ -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:<session>,
|
||||
// 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" },
|
||||
],
|
||||
};
|
||||
73
open-sse/providers/registry/workbuddy.js
Normal file
73
open-sse/providers/registry/workbuddy.js
Normal file
@@ -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,
|
||||
},
|
||||
};
|
||||
72
open-sse/providers/registry/zed.js
Normal file
72
open-sse/providers/registry/zed.js
Normal file
@@ -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: <user_id> <access_token>" 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: "<user_id> <access_token>", // 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,
|
||||
},
|
||||
};
|
||||
@@ -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),
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
416
open-sse/shared/zedAuth.js
Normal file
416
open-sse/shared/zedAuth.js
Normal file
@@ -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();
|
||||
}
|
||||
BIN
public/providers/codebuddy-intl.png
Normal file
BIN
public/providers/codebuddy-intl.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
BIN
public/providers/trae.png
Normal file
BIN
public/providers/trae.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.3 KiB |
BIN
public/providers/windsurf.png
Normal file
BIN
public/providers/windsurf.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.1 KiB |
BIN
public/providers/workbuddy.png
Normal file
BIN
public/providers/workbuddy.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
BIN
public/providers/zed.png
Normal file
BIN
public/providers/zed.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
Reference in New Issue
Block a user