pi-windsurf-beta 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/oauth.ts ADDED
@@ -0,0 +1,290 @@
1
+ /**
2
+ * OAuth login + RegisterUser + credential storage.
3
+ */
4
+ import * as http from "http";
5
+ import * as crypto from "crypto";
6
+ import * as fs from "fs";
7
+ import * as path from "path";
8
+ import * as os from "os";
9
+ import { spawn } from "child_process";
10
+
11
+ // ----------------------------------------------------------------------------
12
+ // Types
13
+ // ----------------------------------------------------------------------------
14
+
15
+ export interface OAuthLoginResult {
16
+ apiKey: string;
17
+ name: string;
18
+ apiServerUrl: string;
19
+ redirectUrl?: string;
20
+ }
21
+
22
+ export interface WindsurfRegion {
23
+ website: string;
24
+ registerApiServerUrl: string;
25
+ oauthClientId: string;
26
+ }
27
+
28
+ export const DEFAULT_REGION: WindsurfRegion = {
29
+ website: "https://windsurf.com",
30
+ registerApiServerUrl: "https://register.windsurf.com",
31
+ oauthClientId: "3GUryQ7ldAeKEuD2obYnppsnmj58eP5u",
32
+ };
33
+
34
+ export interface PersistedCredentials extends OAuthLoginResult {
35
+ issuedAt: string;
36
+ oauthClientId: string;
37
+ }
38
+
39
+ // ----------------------------------------------------------------------------
40
+ // RegisterUser
41
+ // ----------------------------------------------------------------------------
42
+
43
+ function anySignal(signals: AbortSignal[]): AbortSignal {
44
+ const builtin = (AbortSignal as unknown as { any?: (s: AbortSignal[]) => AbortSignal }).any;
45
+ if (typeof builtin === "function") return builtin(signals);
46
+ const controller = new AbortController();
47
+ const onAbort = (reason: unknown): void => {
48
+ if (!controller.signal.aborted) controller.abort(reason);
49
+ };
50
+ for (const s of signals) {
51
+ if (s.aborted) { onAbort(s.reason); break; }
52
+ s.addEventListener("abort", () => onAbort(s.reason), { once: true });
53
+ }
54
+ return controller.signal;
55
+ }
56
+
57
+ export class WindsurfRegistrationError extends Error {
58
+ readonly status: number;
59
+ readonly connectCode?: string;
60
+ readonly traceId?: string;
61
+ constructor(message: string, status: number, connectCode?: string, traceId?: string) {
62
+ super(message);
63
+ this.name = "WindsurfRegistrationError";
64
+ this.status = status;
65
+ this.connectCode = connectCode;
66
+ this.traceId = traceId;
67
+ }
68
+ }
69
+
70
+ const TRACE_ID_RE = /\(trace ID: ([0-9a-f]+)\)/i;
71
+
72
+ export async function registerUser(
73
+ firebaseIdToken: string,
74
+ region: WindsurfRegion,
75
+ abortSignal?: AbortSignal,
76
+ ): Promise<OAuthLoginResult> {
77
+ const url = `${region.registerApiServerUrl.replace(/\/$/, "")}/exa.seat_management_pb.SeatManagementService/RegisterUser`;
78
+ const timeoutSignal = AbortSignal.timeout(30_000);
79
+ const combinedSignal = abortSignal ? anySignal([abortSignal, timeoutSignal]) : timeoutSignal;
80
+
81
+ const response = await fetch(url, {
82
+ method: "POST",
83
+ headers: { "Content-Type": "application/json", "Connect-Protocol-Version": "1" },
84
+ body: JSON.stringify({ firebase_id_token: firebaseIdToken }),
85
+ signal: combinedSignal,
86
+ });
87
+ const text = await response.text();
88
+
89
+ if (!response.ok) {
90
+ let connectCode: string | undefined;
91
+ let message = text || `RegisterUser failed with HTTP ${response.status}`;
92
+ try {
93
+ const errJson = JSON.parse(text) as { code?: string; message?: string };
94
+ connectCode = errJson.code;
95
+ if (errJson.message) message = errJson.message;
96
+ } catch {}
97
+ const traceMatch = message.match(TRACE_ID_RE);
98
+ throw new WindsurfRegistrationError(message, response.status, connectCode, traceMatch?.[1]);
99
+ }
100
+
101
+ const parsed = JSON.parse(text) as { api_key?: string; name?: string; api_server_url?: string; redirect_url?: string };
102
+ const apiKey = parsed.api_key;
103
+ const name = parsed.name;
104
+ const apiServerUrl = parsed.api_server_url && parsed.api_server_url.length > 0 ? parsed.api_server_url : "https://server.codeium.com";
105
+
106
+ if (!apiKey) throw new WindsurfRegistrationError("RegisterUser returned 200 but api_key was empty", response.status, "malformed_response");
107
+ if (!name) throw new WindsurfRegistrationError("RegisterUser returned 200 but name was empty", response.status, "malformed_response");
108
+
109
+ return { apiKey, name, apiServerUrl, redirectUrl: parsed.redirect_url };
110
+ }
111
+
112
+ // ----------------------------------------------------------------------------
113
+ // Credential Storage
114
+ // ----------------------------------------------------------------------------
115
+
116
+ const APP_DIR_NAME = "opencode-windsurf-auth";
117
+ const CREDS_FILENAME = "credentials.json";
118
+
119
+ export function getCredentialsDir(): string {
120
+ return path.join(os.homedir(), ".config", APP_DIR_NAME);
121
+ }
122
+
123
+ export function getCredentialsPath(): string {
124
+ return path.join(getCredentialsDir(), CREDS_FILENAME);
125
+ }
126
+
127
+ function ensureDir(): void {
128
+ const dir = getCredentialsDir();
129
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
130
+ }
131
+
132
+ export function loadCredentials(): PersistedCredentials | null {
133
+ const p = getCredentialsPath();
134
+ if (!fs.existsSync(p)) return null;
135
+ const raw = fs.readFileSync(p, "utf8");
136
+ const parsed = JSON.parse(raw) as Record<string, unknown>;
137
+ if (typeof parsed.apiKey !== "string" || !parsed.apiKey ||
138
+ typeof parsed.name !== "string" || !parsed.name ||
139
+ typeof parsed.apiServerUrl !== "string" || !parsed.apiServerUrl) {
140
+ throw new Error(`Credentials file at ${p} is missing required fields.`);
141
+ }
142
+ return parsed as unknown as PersistedCredentials;
143
+ }
144
+
145
+ export function saveCredentials(creds: PersistedCredentials): void {
146
+ ensureDir();
147
+ const p = getCredentialsPath();
148
+ fs.writeFileSync(p, JSON.stringify(creds, null, 2), { mode: 0o600 });
149
+ }
150
+
151
+ export function deleteCredentials(): boolean {
152
+ const p = getCredentialsPath();
153
+ if (!fs.existsSync(p)) return false;
154
+ fs.unlinkSync(p);
155
+ return true;
156
+ }
157
+
158
+ // ----------------------------------------------------------------------------
159
+ // Login flow — loopback callback
160
+ // ----------------------------------------------------------------------------
161
+
162
+ interface CallbackResult { token: string; state: string; }
163
+
164
+ export async function runLoginLoopback(
165
+ region: WindsurfRegion,
166
+ onUrl: (url: string) => void,
167
+ signal?: AbortSignal,
168
+ ): Promise<string> {
169
+ const state = crypto.randomUUID();
170
+ const server = await startCallbackServer();
171
+ const callbackUrl = `http://127.0.0.1:${server.port}/auth`;
172
+ const loginUrl = buildLoginUrl(region, callbackUrl, "query", state);
173
+
174
+ const callbackPromise = server.callback(state);
175
+ callbackPromise.catch(() => {});
176
+
177
+ onUrl(loginUrl);
178
+ await openBrowser(loginUrl).catch(() => {});
179
+
180
+ const callback = await waitWithTimeout(callbackPromise, 5 * 60 * 1000, signal, "Sign-in timed out.");
181
+
182
+ if (!callback.token) throw new Error("OAuth callback delivered an empty token.");
183
+ server.close();
184
+ return callback.token;
185
+ }
186
+
187
+ function buildLoginUrl(region: WindsurfRegion, redirectUri: string, redirectParametersType: "query" | "fragment", state: string): string {
188
+ const params = new URLSearchParams([
189
+ ["response_type", "token"],
190
+ ["client_id", region.oauthClientId],
191
+ ["redirect_uri", redirectUri],
192
+ ["state", state],
193
+ ["prompt", "login"],
194
+ ["redirect_parameters_type", redirectParametersType],
195
+ ]);
196
+ return `${region.website.replace(/\/$/, "")}/windsurf/signin?${params.toString()}`;
197
+ }
198
+
199
+ interface CallbackServer {
200
+ port: number;
201
+ close: () => void;
202
+ callback: (expectedState: string) => Promise<CallbackResult>;
203
+ }
204
+
205
+ function startCallbackServer(): Promise<CallbackServer> {
206
+ return new Promise((resolve, reject) => {
207
+ let captured: { token: string; state: string } | null = null;
208
+ const waiters: Array<{ state: string; resolve: (r: CallbackResult) => void; reject: (e: Error) => void }> = [];
209
+
210
+ const server = http.createServer((req, res) => {
211
+ const url = new URL(req.url ?? "/", "http://127.0.0.1");
212
+ if (url.pathname !== "/auth") { res.writeHead(404); res.end("Not Found"); return; }
213
+
214
+ const tokenParam = url.searchParams.get("firebase_id_token") ?? url.searchParams.get("access_token") ?? url.searchParams.get("token");
215
+ const stateParam = url.searchParams.get("state") ?? "";
216
+
217
+ if (!tokenParam) {
218
+ const html = `<!doctype html><html><head><meta charset="utf-8"></head><body><script>(function(){var h=window.location.hash.replace(/^#/,'');if(!h){document.body.innerText='No token in URL.';return}window.location.replace('/auth?'+h);})();</script></body></html>`;
219
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
220
+ res.end(html);
221
+ return;
222
+ }
223
+
224
+ const matchedWaiter = waiters.find(w => w.state === stateParam);
225
+ if (!matchedWaiter) {
226
+ renderResponse(res, false, "Unexpected callback — does not match any active sign-in attempt. Close this tab.");
227
+ return;
228
+ }
229
+ captured = { token: tokenParam, state: stateParam };
230
+ renderResponse(res, true, "Sign-in complete — you can close this tab.");
231
+ for (let i = waiters.length - 1; i >= 0; i--) {
232
+ const w = waiters[i];
233
+ if (w.state === captured.state) { w.resolve(captured); waiters.splice(i, 1); }
234
+ }
235
+ });
236
+
237
+ server.on("error", reject);
238
+ server.listen(0, "127.0.0.1", () => {
239
+ const addr = server.address();
240
+ if (!addr || typeof addr === "string") { reject(new Error("Failed to bind")); return; }
241
+ resolve({
242
+ port: addr.port,
243
+ close: () => server.close(),
244
+ callback: (expectedState: string) => new Promise((res, rej) => {
245
+ if (captured) res({ token: captured.token, state: captured.state });
246
+ else waiters.push({ state: expectedState, resolve: res, reject: rej });
247
+ }),
248
+ });
249
+ });
250
+ });
251
+ }
252
+
253
+ function renderResponse(res: http.ServerResponse, ok: boolean, message: string): void {
254
+ const html = `<!doctype html><html><head><meta charset="utf-8"><title>Pi Windsurf</title><style>body{font:14px -apple-system,sans-serif;display:flex;align-items:center;justify-content:center;height:100vh;margin:0;background:#0b0d12;color:#e7e9ee}.card{max-width:520px;padding:28px 32px;border-radius:14px;background:#151823;border:1px solid #232838;text-align:center}h1{font-size:18px;margin:0 0 10px;color:${ok ? "#71d784" : "#ff8585"}}p{margin:6px 0;color:#9aa3b2}</style></head><body><div class="card"><h1>${ok ? "Signed in" : "Sign-in failed"}</h1><p>${escapeHtml(message)}</p></div></body></html>`;
255
+ res.writeHead(ok ? 200 : 400, { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store" });
256
+ res.end(html);
257
+ }
258
+
259
+ function escapeHtml(s: string): string {
260
+ return s.replace(/[&<>"']/g, c => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c] ?? c);
261
+ }
262
+
263
+ async function openBrowser(url: string): Promise<void> {
264
+ const cmds = process.platform === "darwin" ? [{ cmd: "open", args: [url] }]
265
+ : process.platform === "win32" ? [{ cmd: "cmd", args: ["/c", "start", '""', url] }]
266
+ : [{ cmd: "xdg-open", args: [url] }, { cmd: "sensible-browser", args: [url] }];
267
+
268
+ for (const c of cmds) {
269
+ const ok = await new Promise<boolean>(resolve => {
270
+ const child = spawn(c.cmd, c.args, { stdio: "ignore", detached: true });
271
+ child.on("error", () => resolve(false));
272
+ child.on("spawn", () => { child.unref(); resolve(true); });
273
+ });
274
+ if (ok) return;
275
+ }
276
+ throw new Error(`Unable to open browser. Open this URL manually:\n ${url}`);
277
+ }
278
+
279
+ function waitWithTimeout<T>(p: Promise<T>, timeoutMs: number, signal: AbortSignal | undefined, msg: string): Promise<T> {
280
+ return new Promise((resolve, reject) => {
281
+ const onAbort = () => { cleanup(); reject(new Error("Sign-in cancelled.")); };
282
+ const timer = setTimeout(() => { cleanup(); reject(new Error(msg)); }, timeoutMs);
283
+ const cleanup = () => { clearTimeout(timer); if (signal) signal.removeEventListener("abort", onAbort); };
284
+ if (signal) {
285
+ if (signal.aborted) { onAbort(); return; }
286
+ signal.addEventListener("abort", onAbort, { once: true });
287
+ }
288
+ p.then(v => { cleanup(); resolve(v); }, e => { cleanup(); reject(e); });
289
+ });
290
+ }
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "pi-windsurf-beta",
3
+ "version": "0.1.3",
4
+ "description": "Windsurf/Cognition models in Pi — Claude, GPT, Gemini, Kimi, DeepSeek, SWE via your Windsurf subscription",
5
+ "author": "dunghoitao",
6
+ "license": "MIT",
7
+ "keywords": [
8
+ "pi-package",
9
+ "pi",
10
+ "windsurf",
11
+ "cognition",
12
+ "codeium",
13
+ "ai",
14
+ "llm",
15
+ "provider"
16
+ ],
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/khanhdeptraivaicachuong/pi-windsurf-beta.git"
20
+ },
21
+ "pi": {
22
+ "extensions": [
23
+ "./index.ts"
24
+ ]
25
+ },
26
+ "files": [
27
+ "*.ts",
28
+ "README.md",
29
+ "LICENSE"
30
+ ],
31
+ "main": "index.js",
32
+ "scripts": {
33
+ "test": "echo \"Error: no test specified\" && exit 1"
34
+ },
35
+ "type": "commonjs",
36
+ "bugs": {
37
+ "url": "https://github.com/khanhdeptraivaicachuong/pi-windsurf-beta/issues"
38
+ },
39
+ "homepage": "https://github.com/khanhdeptraivaicachuong/pi-windsurf-beta#readme"
40
+ }
package/proxy.ts ADDED
@@ -0,0 +1,350 @@
1
+ /**
2
+ * OpenAI-compatible HTTP proxy → Cognition Connect-RPC.
3
+ *
4
+ * Binds at 127.0.0.1:42100 (or fallback port). Accepts standard
5
+ * /v1/chat/completions and /v1/models requests, translates to
6
+ * WindSurf's cloud-direct GetChatMessage wire format.
7
+ */
8
+ import * as crypto from "crypto";
9
+ import { createServer, type IncomingMessage, type ServerResponse } from "http";
10
+ import { streamChatEvents, CloudChatError, type ChatHistoryItem, type ToolDef } from "./chat";
11
+ import { resolveModelOrPassthrough, getDefaultModel, getCanonicalModels } from "./models";
12
+ import { loadCredentials } from "./oauth";
13
+
14
+ const WINDSURF_PROXY_HOST = "127.0.0.1";
15
+ const WINDSURF_PROXY_PORT = 42100;
16
+
17
+ // Per-process secret — same-process callers use this as Bearer token.
18
+ export const PROXY_SECRET: string = crypto.randomBytes(32).toString("hex");
19
+
20
+ // In-memory credentials cache — set from extension on startup/after login
21
+ export let proxyCredentials: { apiKey: string; apiServerUrl: string } | null = null;
22
+ export function setProxyCredentials(creds: { apiKey: string; apiServerUrl: string } | null): void {
23
+ proxyCredentials = creds;
24
+ }
25
+
26
+ // ----------------------------------------------------------------------------
27
+ // Proxy handler (Node http server)
28
+ // ----------------------------------------------------------------------------
29
+
30
+ interface ChatCompletionRequest {
31
+ model?: string;
32
+ messages: Array<{
33
+ role: string;
34
+ content: string | Array<{ type: string; text?: string; image_url?: { url: string } }>;
35
+ tool_call_id?: string;
36
+ tool_calls?: Array<{ id?: string; type?: string; function?: { name?: string; arguments?: string } }>;
37
+ }>;
38
+ stream?: boolean;
39
+ temperature?: number;
40
+ max_tokens?: number;
41
+ tools?: Array<{ type?: string; function?: { name?: string; description?: string; parameters?: Record<string, unknown> } }>;
42
+ providerOptions?: Record<string, unknown>;
43
+ }
44
+
45
+ function mapMessageToHistoryItem(m: ChatCompletionRequest["messages"][number]): ChatHistoryItem {
46
+ const item: ChatHistoryItem = { role: m.role as ChatHistoryItem["role"], content: m.content as ChatHistoryItem["content"] };
47
+ if (m.role === "tool" && typeof m.tool_call_id === "string" && m.tool_call_id.length > 0) {
48
+ item.tool_call_id = m.tool_call_id;
49
+ }
50
+ if (m.role === "assistant" && Array.isArray(m.tool_calls) && m.tool_calls.length > 0) {
51
+ item.tool_calls = m.tool_calls
52
+ .map(tc => ({ id: typeof tc.id === "string" ? tc.id : "", name: typeof tc.function?.name === "string" ? tc.function.name : "", arguments: typeof tc.function?.arguments === "string" ? tc.function.arguments : "" }))
53
+ .filter(tc => tc.id !== "" && tc.name !== "");
54
+ }
55
+ return item;
56
+ }
57
+
58
+ function extractVariantFromProviderOptions(providerOptions: Record<string, unknown> | undefined): string | undefined {
59
+ if (!providerOptions) return undefined;
60
+ const windsurfRaw = providerOptions["windsurf"];
61
+ const windsurf = windsurfRaw && typeof windsurfRaw === "object" ? (windsurfRaw as Record<string, unknown>) : undefined;
62
+ const pickString = (v: unknown): string | undefined => (typeof v === "string" ? v : undefined);
63
+ return pickString(windsurf?.["variant"]) ?? pickString(windsurf?.["variantID"]) ?? pickString(windsurf?.["variantId"]) ?? pickString(providerOptions["variant"]) ?? pickString(providerOptions["variantID"]) ?? pickString(providerOptions["variantId"]);
64
+ }
65
+
66
+ function openAIError(status: number, message: string, details?: string): object {
67
+ return { status, body: JSON.stringify({ error: { message: details ? `${message}\n${details}` : message, type: "windsurf_error", param: null, code: null } }), contentType: "application/json" };
68
+ }
69
+
70
+ async function authorizeRequest(req: IncomingMessage): Promise<{ status: number; body: string; contentType: string } | null> {
71
+ const authHeader = (req.headers.authorization ?? "") as string;
72
+ if (!authHeader.startsWith("Bearer ")) {
73
+ return { status: 401, body: JSON.stringify({ error: { message: "Unauthorized: missing or malformed Authorization header.", type: "windsurf_error" } }), contentType: "application/json" };
74
+ }
75
+ const presented = authHeader.slice("Bearer ".length);
76
+ const presentedBuf = Buffer.from(presented, "utf8");
77
+
78
+ // Accept per-process secret
79
+ const secretBuf = Buffer.from(PROXY_SECRET, "utf8");
80
+ if (presentedBuf.length === secretBuf.length && crypto.timingSafeEqual(presentedBuf, secretBuf)) return null;
81
+
82
+ // Accept in-memory credentials (synced from Pi's OAuth store)
83
+ if (proxyCredentials?.apiKey) {
84
+ const credBuf = Buffer.from(proxyCredentials.apiKey, "utf8");
85
+ if (presentedBuf.length === credBuf.length && crypto.timingSafeEqual(presentedBuf, credBuf)) return null;
86
+ }
87
+ // Accept persisted apiKey from disk (set by standalone CLI)
88
+ try {
89
+ const creds = loadCredentials();
90
+ if (creds?.apiKey && creds.apiKey !== proxyCredentials?.apiKey) {
91
+ const credBuf = Buffer.from(creds.apiKey, "utf8");
92
+ if (presentedBuf.length === credBuf.length && crypto.timingSafeEqual(presentedBuf, credBuf)) return null;
93
+ }
94
+ } catch {}
95
+
96
+ return { status: 401, body: JSON.stringify({ error: { message: "Unauthorized: Invalid Bearer token.", type: "windsurf_error" } }), contentType: "application/json" };
97
+ }
98
+
99
+ function getBody(req: IncomingMessage): Promise<string> {
100
+ return new Promise((resolve, reject) => {
101
+ const chunks: Buffer[] = [];
102
+ req.on("data", c => chunks.push(Buffer.from(c)));
103
+ req.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
104
+ req.on("error", reject);
105
+ });
106
+ }
107
+
108
+ async function handleRequest(req: IncomingMessage, res: ServerResponse): Promise<void> {
109
+ try {
110
+ const url = new URL(req.url ?? "/", `http://${WINDSURF_PROXY_HOST}`);
111
+
112
+ // /health — unauthenticated
113
+ if (url.pathname === "/health") {
114
+ res.writeHead(200, { "Content-Type": "application/json" });
115
+ res.end(JSON.stringify({ ok: true }));
116
+ return;
117
+ }
118
+
119
+ // Auth gate for everything else
120
+ const authErr = await authorizeRequest(req);
121
+ if (authErr) {
122
+ res.writeHead(authErr.status, { "Content-Type": authErr.contentType });
123
+ res.end(authErr.body);
124
+ return;
125
+ }
126
+
127
+ // /v1/models
128
+ if (url.pathname === "/v1/models" || url.pathname === "/models") {
129
+ const modelIds = getCanonicalModels();
130
+ const data = modelIds.map(id => ({ id, object: "model", created: Math.floor(Date.now() / 1000), owned_by: "windsurf" }));
131
+ res.writeHead(200, { "Content-Type": "application/json" });
132
+ res.end(JSON.stringify({ object: "list", data }));
133
+ return;
134
+ }
135
+
136
+ // /v1/chat/completions
137
+ if (url.pathname === "/v1/chat/completions" || url.pathname === "/chat/completions") {
138
+ if (req.method !== "POST") {
139
+ res.writeHead(405, { "Content-Type": "application/json" });
140
+ res.end(JSON.stringify({ error: { message: "Method not allowed; use POST.", type: "windsurf_error" } }));
141
+ return;
142
+ }
143
+
144
+ const rawBody = await getBody(req);
145
+ let requestBody: ChatCompletionRequest;
146
+ try { requestBody = JSON.parse(rawBody); }
147
+ catch { res.writeHead(400, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: { message: "Malformed JSON." } })); return; }
148
+
149
+ if (!requestBody.messages || !Array.isArray(requestBody.messages)) {
150
+ res.writeHead(400, { "Content-Type": "application/json" });
151
+ res.end(JSON.stringify({ error: { message: "messages must be an array." } }));
152
+ return;
153
+ }
154
+
155
+ const diskCreds = loadCredentials();
156
+ const creds = diskCreds ?? proxyCredentials;
157
+ if (!creds) {
158
+ res.writeHead(503, { "Content-Type": "application/json" });
159
+ res.end(JSON.stringify({ error: { message: "Not authenticated. Run /login windsurf first." } }));
160
+ return;
161
+ }
162
+
163
+ const requestedModel = requestBody.model || getDefaultModel();
164
+ const variantOverride = extractVariantFromProviderOptions(requestBody.providerOptions);
165
+ const resolved = resolveModelOrPassthrough(requestedModel + (variantOverride ? `:${variantOverride}` : ""));
166
+
167
+ const tools: ToolDef[] = (requestBody.tools ?? []).map(t => ({
168
+ name: t.function?.name ?? "unknown",
169
+ description: t.function?.description ?? "",
170
+ parameters: t.function?.parameters ?? {},
171
+ }));
172
+
173
+ const multimodalMessages: ChatHistoryItem[] = requestBody.messages.map(m => mapMessageToHistoryItem(m));
174
+ const requestedMaxTokens = typeof requestBody.max_tokens === "number" && requestBody.max_tokens > 0 ? requestBody.max_tokens : 128_000;
175
+ const isStreaming = requestBody.stream !== false;
176
+
177
+ if (isStreaming) {
178
+ // SSE streaming response
179
+ res.writeHead(200, {
180
+ "Content-Type": "text/event-stream",
181
+ "Cache-Control": "no-cache",
182
+ "Connection": "keep-alive",
183
+ });
184
+
185
+ const responseId = `chatcmpl-${crypto.randomUUID()}`;
186
+ const abort = new AbortController();
187
+ req.on("close", () => { if (!res.writableEnded) abort.abort(); });
188
+
189
+ try {
190
+ let firstChunkSent = false;
191
+ let toolCallIndex = -1;
192
+ const toolIdToIndex = new Map<string, number>();
193
+ let lastToolCallId: string | undefined;
194
+ let finishReason: "stop" | "tool_calls" | "length" | "content_filter" | null = null;
195
+ let usage: { promptTokens?: number; completionTokens?: number; totalTokens?: number } | null = null;
196
+
197
+ for await (const ev of streamChatEvents({
198
+ apiKey: creds.apiKey,
199
+ apiServerUrl: creds.apiServerUrl,
200
+ modelUid: resolved.modelUid,
201
+ messages: multimodalMessages,
202
+ tools: tools.length > 0 ? tools : undefined,
203
+ signal: abort.signal,
204
+ completionOpts: { maxOutputTokens: requestedMaxTokens },
205
+ })) {
206
+ const role = firstChunkSent ? undefined : "assistant";
207
+
208
+ if (ev.kind === "text") {
209
+ const chunk = { id: responseId, object: "chat.completion.chunk", created: Math.floor(Date.now() / 1000), model: requestedModel, choices: [{ index: 0, delta: role ? { role, content: ev.text } : { content: ev.text }, finish_reason: null }] };
210
+ res.write(`data: ${JSON.stringify(chunk)}\n\n`);
211
+ firstChunkSent = true;
212
+ } else if (ev.kind === "reasoning") {
213
+ const chunk = { id: responseId, object: "chat.completion.chunk", created: Math.floor(Date.now() / 1000), model: requestedModel, choices: [{ index: 0, delta: role ? { role, reasoning: ev.text } : { reasoning: ev.text }, finish_reason: null }] };
214
+ res.write(`data: ${JSON.stringify(chunk)}\n\n`);
215
+ firstChunkSent = true;
216
+ } else if (ev.kind === "tool_call_start") {
217
+ toolCallIndex += 1;
218
+ toolIdToIndex.set(ev.id, toolCallIndex);
219
+ lastToolCallId = ev.id;
220
+ const baseDelta = { tool_calls: [{ index: toolCallIndex, id: ev.id, type: "function", function: { name: ev.name, arguments: "" } }] };
221
+ const delta = firstChunkSent ? baseDelta : { role: "assistant", ...baseDelta };
222
+ const chunk = { id: responseId, object: "chat.completion.chunk", created: Math.floor(Date.now() / 1000), model: requestedModel, choices: [{ index: 0, delta, finish_reason: null }] };
223
+ res.write(`data: ${JSON.stringify(chunk)}\n\n`);
224
+ firstChunkSent = true;
225
+ } else if (ev.kind === "tool_call_args") {
226
+ if (lastToolCallId === undefined || toolCallIndex < 0) continue;
227
+ const routeKey = ev.id ?? lastToolCallId;
228
+ const idx = toolIdToIndex.get(routeKey) ?? toolCallIndex;
229
+ const chunk = { id: responseId, object: "chat.completion.chunk", created: Math.floor(Date.now() / 1000), model: requestedModel, choices: [{ index: 0, delta: { tool_calls: [{ index: idx, function: { arguments: ev.argsDelta } }] }, finish_reason: null }] };
230
+ res.write(`data: ${JSON.stringify(chunk)}\n\n`);
231
+ } else if (ev.kind === "finish") {
232
+ finishReason = ev.reason;
233
+ } else if (ev.kind === "usage") {
234
+ usage = { promptTokens: ev.promptTokens, completionTokens: ev.completionTokens, totalTokens: ev.totalTokens };
235
+ }
236
+ }
237
+
238
+ const finalReason = finishReason ?? (toolCallIndex >= 0 ? "tool_calls" : "stop");
239
+ const finishChunk = { id: responseId, object: "chat.completion.chunk", created: Math.floor(Date.now() / 1000), model: requestedModel, choices: [{ index: 0, delta: {}, finish_reason: finalReason }] };
240
+ res.write(`data: ${JSON.stringify(finishChunk)}\n\n`);
241
+
242
+ if (usage) {
243
+ const usageChunk = { id: responseId, object: "chat.completion.chunk", created: Math.floor(Date.now() / 1000), model: requestedModel, choices: [], usage: { prompt_tokens: usage.promptTokens ?? 0, completion_tokens: usage.completionTokens ?? 0, total_tokens: usage.totalTokens ?? 0 } };
244
+ res.write(`data: ${JSON.stringify(usageChunk)}\n\n`);
245
+ }
246
+ res.write("data: [DONE]\n\n");
247
+ res.end();
248
+ } catch (error) {
249
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
250
+ try {
251
+ res.write(`data: ${JSON.stringify({ error: { message: errorMessage } })}\n\n`);
252
+ const fChunk = { id: responseId, object: "chat.completion.chunk", created: Math.floor(Date.now() / 1000), model: requestedModel, choices: [{ index: 0, delta: {}, finish_reason: "stop" }] };
253
+ res.write(`data: ${JSON.stringify(fChunk)}\n\n`);
254
+ res.write("data: [DONE]\n\n");
255
+ res.end();
256
+ } catch { /* socket dead */ }
257
+ }
258
+ } else {
259
+ // Non-streaming response
260
+ let collected = "";
261
+ let finishReason: "stop" | "tool_calls" | "length" | "content_filter" = "stop";
262
+ let usage: { promptTokens?: number; completionTokens?: number; totalTokens?: number } | null = null;
263
+ type CollectedToolCall = { id: string; name: string; args: string };
264
+ const collectedToolCalls: CollectedToolCall[] = [];
265
+ let currentToolCall: CollectedToolCall | null = null;
266
+
267
+ const abort = new AbortController();
268
+ for await (const ev of streamChatEvents({
269
+ apiKey: creds.apiKey,
270
+ apiServerUrl: creds.apiServerUrl,
271
+ modelUid: resolved.modelUid,
272
+ messages: multimodalMessages,
273
+ tools: tools.length > 0 ? tools : undefined,
274
+ completionOpts: { maxOutputTokens: requestedMaxTokens },
275
+ signal: abort.signal,
276
+ })) {
277
+ if (ev.kind === "text") collected += ev.text;
278
+ else if (ev.kind === "tool_call_start") { currentToolCall = { id: ev.id, name: ev.name, args: "" }; collectedToolCalls.push(currentToolCall); }
279
+ else if (ev.kind === "tool_call_args") { if (currentToolCall) currentToolCall.args += ev.argsDelta; }
280
+ else if (ev.kind === "finish") finishReason = ev.reason;
281
+ else if (ev.kind === "usage") usage = { promptTokens: ev.promptTokens, completionTokens: ev.completionTokens, totalTokens: ev.totalTokens };
282
+ }
283
+ if (collectedToolCalls.length > 0 && finishReason === "stop") finishReason = "tool_calls";
284
+
285
+ const assistantMessage = collectedToolCalls.length > 0
286
+ ? { role: "assistant" as const, content: collected, tool_calls: collectedToolCalls.map(tc => ({ id: tc.id, type: "function" as const, function: { name: tc.name, arguments: tc.args } })) }
287
+ : { role: "assistant" as const, content: collected };
288
+
289
+ const resp = {
290
+ id: `chatcmpl-${crypto.randomUUID()}`,
291
+ object: "chat.completion",
292
+ created: Math.floor(Date.now() / 1000),
293
+ model: requestedModel,
294
+ choices: [{ index: 0, message: assistantMessage, finish_reason: finishReason }],
295
+ ...(usage ? { usage: { prompt_tokens: usage.promptTokens ?? 0, completion_tokens: usage.completionTokens ?? 0, total_tokens: usage.totalTokens ?? 0 } } : {}),
296
+ };
297
+ res.writeHead(200, { "Content-Type": "application/json" });
298
+ res.end(JSON.stringify(resp));
299
+ }
300
+ return;
301
+ }
302
+
303
+ res.writeHead(404, { "Content-Type": "application/json" });
304
+ res.end(JSON.stringify({ error: { message: `Unsupported path: ${url.pathname}` } }));
305
+ } catch (error) {
306
+ const message = error instanceof Error ? error.message : String(error);
307
+ try {
308
+ res.writeHead(500, { "Content-Type": "application/json" });
309
+ res.end(JSON.stringify({ error: { message } }));
310
+ } catch {}
311
+ }
312
+ }
313
+
314
+ // ----------------------------------------------------------------------------
315
+ // Server startup
316
+ // ----------------------------------------------------------------------------
317
+
318
+ let serverInstance: ReturnType<typeof createServer> | null = null;
319
+
320
+ export function startProxy(port: number = WINDSURF_PROXY_PORT): Promise<number> {
321
+ if (serverInstance) return Promise.resolve((serverInstance.address() as { port: number }).port);
322
+
323
+ return new Promise((resolve, reject) => {
324
+ const srv = createServer(handleRequest);
325
+ srv.on("error", (err: NodeJS.ErrnoException) => {
326
+ if (err.code === "EADDRINUSE") {
327
+ // Try fallback port
328
+ srv.listen(0, WINDSURF_PROXY_HOST, () => {
329
+ const addr = srv.address() as { port: number };
330
+ serverInstance = srv;
331
+ resolve(addr.port);
332
+ });
333
+ return;
334
+ }
335
+ reject(err);
336
+ });
337
+ srv.listen(port, WINDSURF_PROXY_HOST, () => {
338
+ serverInstance = srv;
339
+ const addr = srv.address() as { port: number };
340
+ resolve(addr.port);
341
+ });
342
+ });
343
+ }
344
+
345
+ export function stopProxy(): void {
346
+ if (serverInstance) {
347
+ try { serverInstance.close(); } catch {}
348
+ serverInstance = null;
349
+ }
350
+ }