@ngotrnghia1811/opencode-windsurf-auth 0.1.0
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/LICENSE +21 -0
- package/README.md +105 -0
- package/dist/__tests__/models.test.d.ts +1 -0
- package/dist/__tests__/models.test.js +38 -0
- package/dist/chat-client.d.ts +23 -0
- package/dist/chat-client.js +329 -0
- package/dist/chat-request.d.ts +24 -0
- package/dist/chat-request.js +118 -0
- package/dist/credentials.d.ts +1 -0
- package/dist/credentials.js +39 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +49 -0
- package/dist/models.d.ts +2 -0
- package/dist/models.js +206 -0
- package/dist/proto.d.ts +21 -0
- package/dist/proto.js +100 -0
- package/dist/thinking-proxy.d.ts +22 -0
- package/dist/thinking-proxy.js +151 -0
- package/dist/windsurf-provider.d.ts +16 -0
- package/dist/windsurf-provider.js +535 -0
- package/package.json +48 -0
|
@@ -0,0 +1,535 @@
|
|
|
1
|
+
import { launchProxyStream } from "./thinking-proxy";
|
|
2
|
+
import { loadWindsurfJwt } from "./credentials";
|
|
3
|
+
import { encodeGetChatMessageRequest } from "./chat-request";
|
|
4
|
+
import { streamGetChatMessage } from "./chat-client";
|
|
5
|
+
function getDevinPath() {
|
|
6
|
+
const p = Bun.which("devin");
|
|
7
|
+
if (!p)
|
|
8
|
+
throw new Error("devin CLI not found — run `devin /login` first");
|
|
9
|
+
return p;
|
|
10
|
+
}
|
|
11
|
+
const ZERO_USAGE = {
|
|
12
|
+
inputTokens: { total: undefined, noCache: undefined, cacheRead: undefined, cacheWrite: undefined },
|
|
13
|
+
outputTokens: { total: undefined, text: undefined, reasoning: undefined },
|
|
14
|
+
};
|
|
15
|
+
const STOP_REASON = { unified: "stop", raw: "stop" };
|
|
16
|
+
const TOOL_CALLS_REASON = { unified: "tool-calls", raw: "tool_use" };
|
|
17
|
+
const ERROR_REASON = { unified: "error", raw: "error" };
|
|
18
|
+
const STREAM_TIMEOUT_MS = 300_000;
|
|
19
|
+
const EMPTY_RESULT_ERROR = new Error("Windsurf produced no content (empty stream — possible backend drop or timeout)");
|
|
20
|
+
function trackContent(tracker, type) {
|
|
21
|
+
if (type === "text-delta" ||
|
|
22
|
+
type === "text-start" ||
|
|
23
|
+
type === "reasoning-delta" ||
|
|
24
|
+
type === "reasoning-start" ||
|
|
25
|
+
type === "tool-call" ||
|
|
26
|
+
type === "tool-input-start") {
|
|
27
|
+
tracker.emitted = true;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function stripDevinBanner(text) {
|
|
31
|
+
const noAnsi = text.replace(/\x1b\[[0-9;]*m/g, "");
|
|
32
|
+
const bannerPatterns = [
|
|
33
|
+
"Welcome to Devin CLI",
|
|
34
|
+
"Logged in as",
|
|
35
|
+
"You're all set",
|
|
36
|
+
"✓ Organization",
|
|
37
|
+
];
|
|
38
|
+
const lines = noAnsi.split("\n");
|
|
39
|
+
let start = 0;
|
|
40
|
+
for (let i = 0; i < lines.length; i++) {
|
|
41
|
+
if (bannerPatterns.some((p) => lines[i].includes(p))) {
|
|
42
|
+
start = i + 1;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
return lines.slice(start).join("\n");
|
|
48
|
+
}
|
|
49
|
+
function flattenHistory(options) {
|
|
50
|
+
const tools = options.tools;
|
|
51
|
+
const toolsPrompt = tools
|
|
52
|
+
? `<tools>\n${Object.entries(tools)
|
|
53
|
+
.map(([name, def]) => ` ${name}: ${def.description ?? name}`)
|
|
54
|
+
.join("\n")}\n</tools>\n\n`
|
|
55
|
+
: "";
|
|
56
|
+
const messages = options.prompt
|
|
57
|
+
.map((msg) => {
|
|
58
|
+
const role = msg.role;
|
|
59
|
+
const parts = typeof msg.content === "string" ? msg.content : msg.content;
|
|
60
|
+
if (typeof parts === "string")
|
|
61
|
+
return `${role}: ${parts}`;
|
|
62
|
+
return `${role}: ${parts
|
|
63
|
+
.map((p) => {
|
|
64
|
+
if (p.type === "text")
|
|
65
|
+
return p.text;
|
|
66
|
+
if (p.type === "tool-result") {
|
|
67
|
+
const tr = p;
|
|
68
|
+
return `[tool result #${tr.toolCallId}: ${JSON.stringify(tr.output)}]`;
|
|
69
|
+
}
|
|
70
|
+
return "";
|
|
71
|
+
})
|
|
72
|
+
.join("")}`;
|
|
73
|
+
})
|
|
74
|
+
.join("\n\n");
|
|
75
|
+
return toolsPrompt + messages;
|
|
76
|
+
}
|
|
77
|
+
// ── Level-2: direct Connect-RPC to Windsurf API ─────────────────────────────
|
|
78
|
+
function extractSystemPrompt(options) {
|
|
79
|
+
for (const msg of options.prompt) {
|
|
80
|
+
if (msg.role === "system")
|
|
81
|
+
return msg.content;
|
|
82
|
+
}
|
|
83
|
+
return "";
|
|
84
|
+
}
|
|
85
|
+
/** Extract a plain string from a LanguageModelV3ToolResultOutput. */
|
|
86
|
+
function extractToolOutput(output) {
|
|
87
|
+
const o = output;
|
|
88
|
+
if (o.type === "text" && typeof o.value === "string")
|
|
89
|
+
return o.value;
|
|
90
|
+
if (o.type === "json")
|
|
91
|
+
return JSON.stringify(o.value);
|
|
92
|
+
if (o.type === "error-text" && typeof o.value === "string")
|
|
93
|
+
return o.value;
|
|
94
|
+
if (o.type === "error-json")
|
|
95
|
+
return JSON.stringify(o.value);
|
|
96
|
+
if (o.type === "execution-denied")
|
|
97
|
+
return `Execution denied${o.reason ? `: ${o.reason}` : ""}`;
|
|
98
|
+
if (o.type === "content")
|
|
99
|
+
return JSON.stringify(o.value);
|
|
100
|
+
return JSON.stringify(output);
|
|
101
|
+
}
|
|
102
|
+
function convertToProtoMessages(options) {
|
|
103
|
+
const result = [];
|
|
104
|
+
for (const msg of options.prompt) {
|
|
105
|
+
if (msg.role === "system")
|
|
106
|
+
continue; // handled separately as system_prompt f2
|
|
107
|
+
if (msg.role === "user") {
|
|
108
|
+
const content = typeof msg.content === "string"
|
|
109
|
+
? msg.content
|
|
110
|
+
: msg.content
|
|
111
|
+
.filter((p) => p.type === "text")
|
|
112
|
+
.map((p) => p.text)
|
|
113
|
+
.join("");
|
|
114
|
+
result.push({ role: 1, content });
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (msg.role === "assistant") {
|
|
118
|
+
const parts = msg.content;
|
|
119
|
+
// Collect text/reasoning parts into a single text message
|
|
120
|
+
const text = parts
|
|
121
|
+
.filter((p) => p.type === "text" || p.type === "reasoning")
|
|
122
|
+
.map((p) => p.text)
|
|
123
|
+
.join("");
|
|
124
|
+
// Emit tool-call parts as separate role=2 messages with f6
|
|
125
|
+
const toolCalls = parts.filter((p) => p.type === "tool-call");
|
|
126
|
+
// If there's text, emit as standalone role=2 text message.
|
|
127
|
+
// Prefer separate from tool-call messages when both are present.
|
|
128
|
+
if (text) {
|
|
129
|
+
result.push({ role: 2, content: text });
|
|
130
|
+
}
|
|
131
|
+
// Emit each tool-call as a pure role=2 tool-call message (no text attached)
|
|
132
|
+
for (const tc of toolCalls) {
|
|
133
|
+
const argumentsJson = typeof tc.input === "string"
|
|
134
|
+
? tc.input
|
|
135
|
+
: JSON.stringify(tc.input);
|
|
136
|
+
result.push({
|
|
137
|
+
role: 2,
|
|
138
|
+
toolCall: { id: tc.toolCallId, name: tc.toolName, argumentsJson },
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (msg.role === "tool") {
|
|
144
|
+
const parts = msg.content;
|
|
145
|
+
for (const tr of parts) {
|
|
146
|
+
if (tr.type !== "tool-result")
|
|
147
|
+
continue;
|
|
148
|
+
result.push({
|
|
149
|
+
role: 4,
|
|
150
|
+
content: extractToolOutput(tr.output),
|
|
151
|
+
toolResult: { toolCallId: tr.toolCallId },
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return result;
|
|
158
|
+
}
|
|
159
|
+
function convertTools(options) {
|
|
160
|
+
const tools = options.tools;
|
|
161
|
+
if (!tools)
|
|
162
|
+
return [];
|
|
163
|
+
return tools
|
|
164
|
+
.filter((t) => t.type === "function")
|
|
165
|
+
.map((t) => {
|
|
166
|
+
const ft = t;
|
|
167
|
+
return {
|
|
168
|
+
name: ft.name,
|
|
169
|
+
description: ft.description ?? ft.name,
|
|
170
|
+
parametersJsonSchema: JSON.stringify(ft.inputSchema),
|
|
171
|
+
};
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
async function streamViaDirectConnect(controller, options, modelId, tracker, signal) {
|
|
175
|
+
if (signal?.aborted)
|
|
176
|
+
return false;
|
|
177
|
+
const jwt = await loadWindsurfJwt();
|
|
178
|
+
if (!jwt)
|
|
179
|
+
return false;
|
|
180
|
+
const systemPrompt = extractSystemPrompt(options);
|
|
181
|
+
const messages = convertToProtoMessages(options);
|
|
182
|
+
const tools = convertTools(options);
|
|
183
|
+
const body = encodeGetChatMessageRequest({
|
|
184
|
+
jwt,
|
|
185
|
+
systemPrompt,
|
|
186
|
+
messages,
|
|
187
|
+
tools,
|
|
188
|
+
modelId,
|
|
189
|
+
});
|
|
190
|
+
let reasoningStarted = false;
|
|
191
|
+
let textStarted = false;
|
|
192
|
+
let finished = false;
|
|
193
|
+
const toolCalls = new Map();
|
|
194
|
+
let toolCallStarted = false;
|
|
195
|
+
const events = streamGetChatMessage(body, signal);
|
|
196
|
+
function flushToolCall(id) {
|
|
197
|
+
const tc = toolCalls.get(id);
|
|
198
|
+
if (!tc)
|
|
199
|
+
return;
|
|
200
|
+
toolCalls.delete(id);
|
|
201
|
+
const input = tc.argsChunks.join("");
|
|
202
|
+
controller.enqueue({ type: "tool-input-end", id });
|
|
203
|
+
controller.enqueue({ type: "tool-call", toolCallId: id, toolName: tc.name, input });
|
|
204
|
+
trackContent(tracker, "tool-call");
|
|
205
|
+
}
|
|
206
|
+
function flushAllToolCalls() {
|
|
207
|
+
for (const id of toolCalls.keys()) {
|
|
208
|
+
flushToolCall(id);
|
|
209
|
+
}
|
|
210
|
+
toolCallStarted = false;
|
|
211
|
+
}
|
|
212
|
+
try {
|
|
213
|
+
for await (const event of events) {
|
|
214
|
+
if (finished)
|
|
215
|
+
break;
|
|
216
|
+
switch (event.type) {
|
|
217
|
+
case "reasoning": {
|
|
218
|
+
if (!reasoningStarted) {
|
|
219
|
+
controller.enqueue({ type: "reasoning-start", id: "0" });
|
|
220
|
+
trackContent(tracker, "reasoning-start");
|
|
221
|
+
reasoningStarted = true;
|
|
222
|
+
}
|
|
223
|
+
controller.enqueue({ type: "reasoning-delta", id: "0", delta: event.delta });
|
|
224
|
+
trackContent(tracker, "reasoning-delta");
|
|
225
|
+
break;
|
|
226
|
+
}
|
|
227
|
+
case "text": {
|
|
228
|
+
if (reasoningStarted) {
|
|
229
|
+
controller.enqueue({ type: "reasoning-end", id: "0" });
|
|
230
|
+
reasoningStarted = false;
|
|
231
|
+
}
|
|
232
|
+
if (!textStarted) {
|
|
233
|
+
controller.enqueue({ type: "text-start", id: "1" });
|
|
234
|
+
trackContent(tracker, "text-start");
|
|
235
|
+
textStarted = true;
|
|
236
|
+
}
|
|
237
|
+
controller.enqueue({ type: "text-delta", id: "1", delta: event.delta });
|
|
238
|
+
trackContent(tracker, "text-delta");
|
|
239
|
+
break;
|
|
240
|
+
}
|
|
241
|
+
case "tool-call-start": {
|
|
242
|
+
// close any prior text/reasoning stream
|
|
243
|
+
if (reasoningStarted) {
|
|
244
|
+
controller.enqueue({ type: "reasoning-end", id: "0" });
|
|
245
|
+
reasoningStarted = false;
|
|
246
|
+
}
|
|
247
|
+
if (textStarted) {
|
|
248
|
+
controller.enqueue({ type: "text-end", id: "1" });
|
|
249
|
+
textStarted = false;
|
|
250
|
+
}
|
|
251
|
+
// flush any previous incomplete tool call
|
|
252
|
+
flushAllToolCalls();
|
|
253
|
+
// start new tool call
|
|
254
|
+
toolCalls.set(event.id, { name: event.name, argsChunks: [] });
|
|
255
|
+
controller.enqueue({ type: "tool-input-start", id: event.id, toolName: event.name });
|
|
256
|
+
trackContent(tracker, "tool-input-start");
|
|
257
|
+
toolCallStarted = true;
|
|
258
|
+
break;
|
|
259
|
+
}
|
|
260
|
+
case "tool-call-delta": {
|
|
261
|
+
const tc = toolCalls.get(event.id);
|
|
262
|
+
if (tc) {
|
|
263
|
+
tc.argsChunks.push(event.argsChunk);
|
|
264
|
+
}
|
|
265
|
+
controller.enqueue({ type: "tool-input-delta", id: event.id, delta: event.argsChunk });
|
|
266
|
+
break;
|
|
267
|
+
}
|
|
268
|
+
case "finish": {
|
|
269
|
+
finished = true;
|
|
270
|
+
if (reasoningStarted) {
|
|
271
|
+
controller.enqueue({ type: "reasoning-end", id: "0" });
|
|
272
|
+
reasoningStarted = false;
|
|
273
|
+
}
|
|
274
|
+
if (textStarted) {
|
|
275
|
+
controller.enqueue({ type: "text-end", id: "1" });
|
|
276
|
+
textStarted = false;
|
|
277
|
+
}
|
|
278
|
+
// flush any pending tool calls
|
|
279
|
+
flushAllToolCalls();
|
|
280
|
+
const isToolUse = event.stopReason === 10;
|
|
281
|
+
const usage = {
|
|
282
|
+
inputTokens: {
|
|
283
|
+
total: event.inputTokens,
|
|
284
|
+
noCache: undefined,
|
|
285
|
+
cacheRead: undefined,
|
|
286
|
+
cacheWrite: undefined,
|
|
287
|
+
},
|
|
288
|
+
outputTokens: {
|
|
289
|
+
total: event.outputTokens,
|
|
290
|
+
text: event.outputTokens,
|
|
291
|
+
reasoning: undefined,
|
|
292
|
+
},
|
|
293
|
+
};
|
|
294
|
+
controller.enqueue({
|
|
295
|
+
type: "finish",
|
|
296
|
+
finishReason: isToolUse ? TOOL_CALLS_REASON : STOP_REASON,
|
|
297
|
+
usage,
|
|
298
|
+
});
|
|
299
|
+
break;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
catch {
|
|
305
|
+
return false;
|
|
306
|
+
}
|
|
307
|
+
if (!finished) {
|
|
308
|
+
if (reasoningStarted)
|
|
309
|
+
controller.enqueue({ type: "reasoning-end", id: "0" });
|
|
310
|
+
if (textStarted)
|
|
311
|
+
controller.enqueue({ type: "text-end", id: "1" });
|
|
312
|
+
flushAllToolCalls();
|
|
313
|
+
controller.enqueue({ type: "finish", finishReason: STOP_REASON, usage: ZERO_USAGE });
|
|
314
|
+
}
|
|
315
|
+
if (!tracker.emitted) {
|
|
316
|
+
controller.enqueue({ type: "error", error: EMPTY_RESULT_ERROR });
|
|
317
|
+
}
|
|
318
|
+
controller.close();
|
|
319
|
+
return true;
|
|
320
|
+
}
|
|
321
|
+
// ── Fallback: devin -p path ──────────────────────────────────────────────────
|
|
322
|
+
async function fallbackDevinRun(modelId, prompt) {
|
|
323
|
+
const proc = Bun.spawn([getDevinPath(), "--permission-mode", "bypass", "--model", modelId, "-p", "--", prompt], {
|
|
324
|
+
stdout: "pipe",
|
|
325
|
+
stderr: "pipe",
|
|
326
|
+
});
|
|
327
|
+
const output = await new Response(proc.stdout).text();
|
|
328
|
+
const stderr = await new Response(proc.stderr).text();
|
|
329
|
+
const exitCode = await proc.exited;
|
|
330
|
+
return { output, exitCode, stderr };
|
|
331
|
+
}
|
|
332
|
+
async function doGenerateViaFallback(options, modelId) {
|
|
333
|
+
const prompt = flattenHistory(options);
|
|
334
|
+
const { output, exitCode, stderr } = await fallbackDevinRun(modelId, prompt);
|
|
335
|
+
if (exitCode !== 0) {
|
|
336
|
+
throw new Error(`devin exited with code ${exitCode}: ${stderr.slice(0, 500)}`);
|
|
337
|
+
}
|
|
338
|
+
return {
|
|
339
|
+
content: [{ type: "text", text: stripDevinBanner(output) }],
|
|
340
|
+
finishReason: STOP_REASON,
|
|
341
|
+
usage: ZERO_USAGE,
|
|
342
|
+
warnings: [],
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
// ── Proxy streaming path (best-effort reasoning enrichment) ─────────────────
|
|
346
|
+
async function streamViaProxy(controller, events, tracker) {
|
|
347
|
+
let reasoningStarted = false;
|
|
348
|
+
let textStarted = false;
|
|
349
|
+
let finished = false;
|
|
350
|
+
try {
|
|
351
|
+
for await (const event of events) {
|
|
352
|
+
if (finished)
|
|
353
|
+
break;
|
|
354
|
+
switch (event.type) {
|
|
355
|
+
case "reasoning": {
|
|
356
|
+
const t = event.text;
|
|
357
|
+
if (!reasoningStarted) {
|
|
358
|
+
controller.enqueue({ type: "reasoning-start", id: "0" });
|
|
359
|
+
trackContent(tracker, "reasoning-start");
|
|
360
|
+
reasoningStarted = true;
|
|
361
|
+
}
|
|
362
|
+
controller.enqueue({ type: "reasoning-delta", id: "0", delta: t });
|
|
363
|
+
trackContent(tracker, "reasoning-delta");
|
|
364
|
+
break;
|
|
365
|
+
}
|
|
366
|
+
case "text": {
|
|
367
|
+
const t = event.text;
|
|
368
|
+
if (reasoningStarted) {
|
|
369
|
+
controller.enqueue({ type: "reasoning-end", id: "0" });
|
|
370
|
+
reasoningStarted = false;
|
|
371
|
+
}
|
|
372
|
+
if (!textStarted) {
|
|
373
|
+
controller.enqueue({ type: "text-start", id: "1" });
|
|
374
|
+
trackContent(tracker, "text-start");
|
|
375
|
+
textStarted = true;
|
|
376
|
+
}
|
|
377
|
+
controller.enqueue({ type: "text-delta", id: "1", delta: t });
|
|
378
|
+
trackContent(tracker, "text-delta");
|
|
379
|
+
break;
|
|
380
|
+
}
|
|
381
|
+
case "finish": {
|
|
382
|
+
finished = true;
|
|
383
|
+
const f = event;
|
|
384
|
+
if (reasoningStarted) {
|
|
385
|
+
controller.enqueue({ type: "reasoning-end", id: "0" });
|
|
386
|
+
reasoningStarted = false;
|
|
387
|
+
}
|
|
388
|
+
if (textStarted) {
|
|
389
|
+
controller.enqueue({ type: "text-end", id: "1" });
|
|
390
|
+
textStarted = false;
|
|
391
|
+
}
|
|
392
|
+
const usage = {
|
|
393
|
+
inputTokens: {
|
|
394
|
+
total: f.input_tokens,
|
|
395
|
+
noCache: undefined,
|
|
396
|
+
cacheRead: undefined,
|
|
397
|
+
cacheWrite: undefined,
|
|
398
|
+
},
|
|
399
|
+
outputTokens: {
|
|
400
|
+
total: f.output_tokens,
|
|
401
|
+
text: f.output_tokens,
|
|
402
|
+
reasoning: undefined,
|
|
403
|
+
},
|
|
404
|
+
};
|
|
405
|
+
controller.enqueue({ type: "finish", finishReason: STOP_REASON, usage });
|
|
406
|
+
break;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
catch {
|
|
412
|
+
return false;
|
|
413
|
+
}
|
|
414
|
+
// If we never got a finish event, emit one with zero usage
|
|
415
|
+
if (!finished) {
|
|
416
|
+
if (reasoningStarted)
|
|
417
|
+
controller.enqueue({ type: "reasoning-end", id: "0" });
|
|
418
|
+
if (textStarted)
|
|
419
|
+
controller.enqueue({ type: "text-end", id: "1" });
|
|
420
|
+
controller.enqueue({ type: "finish", finishReason: STOP_REASON, usage: ZERO_USAGE });
|
|
421
|
+
}
|
|
422
|
+
if (!tracker.emitted) {
|
|
423
|
+
controller.enqueue({ type: "error", error: EMPTY_RESULT_ERROR });
|
|
424
|
+
}
|
|
425
|
+
controller.close();
|
|
426
|
+
return true;
|
|
427
|
+
}
|
|
428
|
+
// ── Fallback: direct devin -p word-split (no reasoning) ─────────────────────
|
|
429
|
+
async function streamViaFallback(controller, modelId, text, tracker, signal) {
|
|
430
|
+
if (signal?.aborted) {
|
|
431
|
+
controller.enqueue({ type: "error", error: EMPTY_RESULT_ERROR });
|
|
432
|
+
controller.enqueue({ type: "finish", finishReason: ERROR_REASON, usage: ZERO_USAGE });
|
|
433
|
+
controller.close();
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
const proc = Bun.spawn([getDevinPath(), "--permission-mode", "bypass", "--model", modelId, "-p", "--", text], {
|
|
437
|
+
stdout: "pipe",
|
|
438
|
+
stderr: "pipe",
|
|
439
|
+
signal,
|
|
440
|
+
});
|
|
441
|
+
const output = await new Response(proc.stdout).text();
|
|
442
|
+
const stripped = stripDevinBanner(output);
|
|
443
|
+
const wordPattern = /\S+\s*/g;
|
|
444
|
+
const words = stripped.match(wordPattern) ?? [];
|
|
445
|
+
controller.enqueue({ type: "text-start", id: "0" });
|
|
446
|
+
trackContent(tracker, "text-start");
|
|
447
|
+
for (const word of words) {
|
|
448
|
+
controller.enqueue({ type: "text-delta", id: "0", delta: word });
|
|
449
|
+
trackContent(tracker, "text-delta");
|
|
450
|
+
}
|
|
451
|
+
const exitCode = await proc.exited;
|
|
452
|
+
if (exitCode !== 0) {
|
|
453
|
+
const stderr = await new Response(proc.stderr).text();
|
|
454
|
+
controller.enqueue({ type: "error", error: new Error(`devin exit ${exitCode}: ${stderr.slice(0, 200)}`) });
|
|
455
|
+
controller.enqueue({ type: "finish", finishReason: ERROR_REASON, usage: ZERO_USAGE });
|
|
456
|
+
controller.close();
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
controller.enqueue({ type: "text-end", id: "0" });
|
|
460
|
+
controller.enqueue({ type: "finish", finishReason: STOP_REASON, usage: ZERO_USAGE });
|
|
461
|
+
if (!tracker.emitted) {
|
|
462
|
+
controller.enqueue({ type: "error", error: EMPTY_RESULT_ERROR });
|
|
463
|
+
}
|
|
464
|
+
controller.close();
|
|
465
|
+
}
|
|
466
|
+
// ── Model class ──────────────────────────────────────────────────────────────
|
|
467
|
+
export class WindsurfLanguageModel {
|
|
468
|
+
specificationVersion = "v3";
|
|
469
|
+
provider;
|
|
470
|
+
modelId;
|
|
471
|
+
supportedUrls = {};
|
|
472
|
+
constructor(modelId) {
|
|
473
|
+
this.provider = "windsurf";
|
|
474
|
+
this.modelId = modelId;
|
|
475
|
+
}
|
|
476
|
+
async doGenerate(options) {
|
|
477
|
+
return doGenerateViaFallback(options, this.modelId);
|
|
478
|
+
}
|
|
479
|
+
async doStream(options) {
|
|
480
|
+
const text = flattenHistory(options);
|
|
481
|
+
const modelId = this.modelId;
|
|
482
|
+
const stream = new ReadableStream({
|
|
483
|
+
async start(controller) {
|
|
484
|
+
const tracker = { emitted: false };
|
|
485
|
+
// Combined abort signal: internal timeout + external signal from caller
|
|
486
|
+
const timeoutAc = new AbortController();
|
|
487
|
+
const timeoutId = setTimeout(() => timeoutAc.abort(new Error("Stream timeout")), STREAM_TIMEOUT_MS);
|
|
488
|
+
const externalSignal = options.abortSignal;
|
|
489
|
+
const onExternalAbort = () => {
|
|
490
|
+
clearTimeout(timeoutId);
|
|
491
|
+
timeoutAc.abort(externalSignal?.reason);
|
|
492
|
+
};
|
|
493
|
+
if (externalSignal) {
|
|
494
|
+
if (externalSignal.aborted) {
|
|
495
|
+
clearTimeout(timeoutId);
|
|
496
|
+
timeoutAc.abort(externalSignal.reason);
|
|
497
|
+
}
|
|
498
|
+
externalSignal.addEventListener("abort", onExternalAbort, { once: true });
|
|
499
|
+
}
|
|
500
|
+
try {
|
|
501
|
+
controller.enqueue({ type: "stream-start", warnings: [] });
|
|
502
|
+
// Level-2: direct Connect-RPC to Windsurf API (with tools)
|
|
503
|
+
const directSuccess = await streamViaDirectConnect(controller, options, modelId, tracker, timeoutAc.signal);
|
|
504
|
+
if (directSuccess)
|
|
505
|
+
return;
|
|
506
|
+
// Try proxy path first (best-effort reasoning enrichment)
|
|
507
|
+
const proxyStream = await launchProxyStream(modelId, text);
|
|
508
|
+
if (proxyStream.ok) {
|
|
509
|
+
const success = await streamViaProxy(controller, proxyStream.events, tracker);
|
|
510
|
+
await proxyStream.cleanup();
|
|
511
|
+
if (success)
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
// Fallback: direct devin -p word-split (no reasoning)
|
|
515
|
+
await streamViaFallback(controller, modelId, text, tracker, timeoutAc.signal);
|
|
516
|
+
}
|
|
517
|
+
finally {
|
|
518
|
+
clearTimeout(timeoutId);
|
|
519
|
+
if (externalSignal) {
|
|
520
|
+
externalSignal.removeEventListener("abort", onExternalAbort);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
},
|
|
524
|
+
});
|
|
525
|
+
return { stream };
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
export function createWindsurf(opts) {
|
|
529
|
+
return {
|
|
530
|
+
languageModel(modelId) {
|
|
531
|
+
return new WindsurfLanguageModel(modelId);
|
|
532
|
+
},
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
export * as WindsurfProvider from ".";
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ngotrnghia1811/opencode-windsurf-auth",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"publishConfig": {
|
|
6
|
+
"access": "public"
|
|
7
|
+
},
|
|
8
|
+
"keywords": ["opencode", "windsurf", "llm", "provider", "plugin", "ai"],
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "https://github.com/ngotrnghia1811/opencode-windsurf-auth"
|
|
12
|
+
},
|
|
13
|
+
"main": "./dist/index.js",
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"dependencies": {},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"@ai-sdk/provider": "3.0.8",
|
|
18
|
+
"@opencode-ai/plugin": "1.14.50",
|
|
19
|
+
"@opencode-ai/sdk": "1.15.12",
|
|
20
|
+
"@tsconfig/bun": "1.0.10",
|
|
21
|
+
"@types/bun": "1.3.14",
|
|
22
|
+
"typescript": "6.0.3"
|
|
23
|
+
},
|
|
24
|
+
"peerDependencies": {
|
|
25
|
+
"@opencode-ai/plugin": "*"
|
|
26
|
+
},
|
|
27
|
+
"description": "OpenCode plugin — Level-2 Connect-RPC provider for Windsurf/Cascade models via Devin CLI credentials",
|
|
28
|
+
"engines": {
|
|
29
|
+
"bun": "1.3.14"
|
|
30
|
+
},
|
|
31
|
+
"files": [
|
|
32
|
+
"dist",
|
|
33
|
+
"README.md",
|
|
34
|
+
"LICENSE"
|
|
35
|
+
],
|
|
36
|
+
"exports": {
|
|
37
|
+
".": {
|
|
38
|
+
"import": "./dist/index.js"
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
"scripts": {
|
|
42
|
+
"prepare": "tsc -p tsconfig.build.json",
|
|
43
|
+
"build": "tsc -p tsconfig.build.json",
|
|
44
|
+
"typecheck": "tsc",
|
|
45
|
+
"test": "bun test",
|
|
46
|
+
"dev": "bun scripts/dev.ts"
|
|
47
|
+
}
|
|
48
|
+
}
|