@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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 nghiango
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,105 @@
1
+ # opencode-windsurf-auth
2
+
3
+ OpenCode plugin providing a **Level-2 Connect-RPC provider** for Windsurf/Cascade
4
+ models (Claude, GPT, Gemini, Grok, DeepSeek, and more) via the **Devin CLI**
5
+ credentials flow.
6
+
7
+ > **Warning:** This plugin comes with no guarantees. You might be banned for
8
+ > breaking the Windsurf/Devin Terms of Service. Use at your own risk.
9
+
10
+ ## What This Plugin Does
11
+
12
+ 1. Reads the Windsurf JWT from `~/.local/share/devin/credentials.toml`
13
+ (stored by the Devin CLI after `devin /login`).
14
+ 2. Authenticates against Codeium's HTTP/2 Connect-RPC endpoint
15
+ (`server.codeium.com/exa.api_server_pb.ApiServerService/GetChatMessage`).
16
+ 3. Provides a full `LanguageModelV3` implementation (`createWindsurf`) so
17
+ opencode can use Windsurf models through its standard provider SDK.
18
+ 4. Registers ~130+ models (Claude Opus/Sonnet/Haiku, GPT-5.x, Gemini 3.x,
19
+ Grok, DeepSeek, Kimi, GLM, MiniMax, SWE, and Windsurf-native models).
20
+
21
+ ## Prerequisites
22
+
23
+ - **[Devin CLI](https://devin.ai)** installed and authenticated:
24
+ ```bash
25
+ devin /login
26
+ ```
27
+ - **[mitmproxy](https://mitmproxy.org/)** (specifically `mitmdump`) for
28
+ the thinking-proxy fallback path. Install via pip/brew:
29
+ ```bash
30
+ pip install mitmproxy
31
+ ```
32
+ - **[Bun](https://bun.sh)** >= 1.3.14
33
+ - **[OpenCode](https://github.com/anomalyco/opencode)** with plugin support
34
+
35
+ ## Configuration
36
+
37
+ Add the plugin to your `opencode.json`:
38
+
39
+ ```json
40
+ {
41
+ "plugin": ["file:///path/to/opencode-windsurf-auth/dist/index.js"]
42
+ }
43
+ ```
44
+
45
+ Or for npm (once published):
46
+
47
+ ```json
48
+ {
49
+ "plugin": ["opencode-windsurf-auth"]
50
+ }
51
+ ```
52
+
53
+ ## Provider Registration
54
+
55
+ The provider SDK uses the `providerID` `windsurf-devin-provider`. The model
56
+ entries in `models.ts` expose `api.npm` pointing to this package (via `file://`
57
+ for local dev, or the package name for published use).
58
+
59
+ The package exports both:
60
+ - **`createWindsurf`** (named) — consumed by the provider SDK loader's
61
+ `create*` key scan.
62
+ - **default export (V1 plugin)** — `{ id: "windsurf-auth", server: WindsurfPlugin }`
63
+ consumed by the plugin loader's `readV1Plugin` path.
64
+
65
+ These two consumers look at different slots on the module namespace and do
66
+ not conflict.
67
+
68
+ ## Architecture
69
+
70
+ ```
71
+ src/
72
+ ├── index.ts # Entry point: createWindsurf + V1 plugin default
73
+ ├── windsurf-provider.ts # LanguageModelV3 impl (doGenerate / doStream)
74
+ ├── chat-client.ts # HTTP/2 Connect-RPC streaming client
75
+ ├── chat-request.ts # Protobuf GetChatMessageRequest encoder
76
+ ├── proto.ts # Low-level varint/field/Connect-frame helpers
77
+ ├── credentials.ts # JWT loader from Devin CLI credentials.toml
78
+ ├── models.ts # ~130+ model definitions
79
+ └── thinking-proxy.ts # mitmproxy-based fallback for reasoning/thinking
80
+ ```
81
+
82
+ ### Transport
83
+
84
+ - **Primary path**: Direct HTTP/2 to `server.codeium.com` via Bun's
85
+ built-in ALPN-aware `fetch()`.
86
+ - **Fallback path** (thinking-aware): `thinking-proxy.ts` spawns
87
+ `mitmdump` + `devin -p` and tails a JSONLines sink.
88
+
89
+ ## Development
90
+
91
+ ```bash
92
+ bun install
93
+ bun typecheck # type-check the source
94
+ bun run build # compile to dist/
95
+ bun test # run unit tests
96
+ bun run dev # build + symlink into .opencode/plugins/ + watch
97
+ ```
98
+
99
+ The `bun run dev` script creates a symlink at
100
+ `.opencode/plugins/windsurf-auth.js` → `dist/index.js` so opencode can
101
+ load the plugin via `file://` while you iterate.
102
+
103
+ ## License
104
+
105
+ MIT
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,38 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { WINDSURF_MODELS } from "../models";
3
+ describe("WINDSURF_MODELS", () => {
4
+ test("record is non-empty", () => {
5
+ const entries = Object.entries(WINDSURF_MODELS);
6
+ expect(entries.length).toBeGreaterThan(0);
7
+ });
8
+ test("every model has id starting with windsurf/", () => {
9
+ for (const model of Object.values(WINDSURF_MODELS)) {
10
+ expect(model.id.startsWith("windsurf/")).toBe(true);
11
+ }
12
+ });
13
+ test("every model has api.npm containing windsurf-auth", () => {
14
+ for (const [_, model] of Object.entries(WINDSURF_MODELS)) {
15
+ expect(model.api.npm).toBeString();
16
+ expect(model.api.npm.includes("windsurf-auth")).toBe(true);
17
+ }
18
+ });
19
+ test("every model has a non-empty api.id", () => {
20
+ for (const model of Object.values(WINDSURF_MODELS)) {
21
+ expect(model.api.id.length).toBeGreaterThan(0);
22
+ }
23
+ });
24
+ test("summary", () => {
25
+ const entries = Object.entries(WINDSURF_MODELS);
26
+ const counts = {};
27
+ for (const [, model] of entries) {
28
+ const prefix = model.id.split("/")[1]?.split("-")[0] ?? "unknown";
29
+ counts[prefix] = (counts[prefix] ?? 0) + 1;
30
+ }
31
+ console.log(`\nTotal models: ${entries.length}`);
32
+ console.log("By id prefix:");
33
+ for (const [prefix, count] of Object.entries(counts).sort(([, a], [, b]) => b - a)) {
34
+ console.log(` ${prefix}: ${count}`);
35
+ }
36
+ expect(entries.length).toBeGreaterThan(100);
37
+ });
38
+ });
@@ -0,0 +1,23 @@
1
+ export type ResponseEvent = {
2
+ type: "text";
3
+ delta: string;
4
+ } | {
5
+ type: "reasoning";
6
+ delta: string;
7
+ } | {
8
+ type: "tool-call-start";
9
+ id: string;
10
+ name: string;
11
+ } | {
12
+ type: "tool-call-delta";
13
+ id: string;
14
+ argsChunk: string;
15
+ } | {
16
+ type: "finish";
17
+ model?: string;
18
+ inputTokens?: number;
19
+ outputTokens?: number;
20
+ msgId?: string;
21
+ stopReason?: number;
22
+ };
23
+ export declare function streamGetChatMessage(requestBody: Uint8Array, signal?: AbortSignal): AsyncGenerator<ResponseEvent>;
@@ -0,0 +1,329 @@
1
+ // chat-client.ts — HTTP/2 Connect-RPC streaming client for Windsurf GetChatMessage
2
+ //
3
+ // Transport: Bun fetch with HTTPS (auto-negotiates HTTP/2 via ALPN).
4
+ // Bun's fetch supports streaming response bodies via response.body.getReader().
5
+ // This is the preferred approach over node:http2 because:
6
+ // 1. Bun fetch negotiates HTTP/2 automatically for HTTPS URLs
7
+ // 2. It provides a clean ReadableStream API for reading response bytes
8
+ // 3. No manual ALPN / TLS configuration needed
9
+ // Verified: earlier Python replay experiments used raw h2 connection and
10
+ // confirmed the server accepts HTTP/2 with Connect-RPC content-type.
11
+ //
12
+ // Response field map from:
13
+ // opencode-windsurf-auth/research/connect_decode.py
14
+ // GetChatMessageResponse: f3=delta_text, f9=delta_thinking, f5=stop_reason,
15
+ // f10+f21=="anthropic"=signature frame, f7=stats sub-msg
16
+ import { parseConnectFrames, CONNECT_FLAG_EOS } from "./proto";
17
+ const ENDPOINT = "https://server.codeium.com/exa.api_server_pb.ApiServerService/GetChatMessage";
18
+ // ---------------------------------------------------------------------------
19
+ // Low-level protobuf field walking (mirrors connect_decode.py)
20
+ // ---------------------------------------------------------------------------
21
+ function parseVarint(buf, offset) {
22
+ let result = 0;
23
+ let shift = 0;
24
+ while (true) {
25
+ const byte = buf[offset];
26
+ offset++;
27
+ result |= (byte & 0x7f) << shift;
28
+ if (!(byte & 0x80))
29
+ break;
30
+ shift += 7;
31
+ }
32
+ return [result, offset];
33
+ }
34
+ function walkFields(body) {
35
+ const fields = [];
36
+ let i = 0;
37
+ while (i < body.byteLength) {
38
+ const [tag, newI] = parseVarint(body, i);
39
+ i = newI;
40
+ const field = tag >> 3;
41
+ const wire = tag & 7;
42
+ if (wire === 0) {
43
+ const [v, ni] = parseVarint(body, i);
44
+ i = ni;
45
+ fields.push({ field, wire, value: v });
46
+ }
47
+ else if (wire === 2) {
48
+ const [ln, ni] = parseVarint(body, i);
49
+ i = ni;
50
+ const val = body.slice(i, i + ln);
51
+ i += ln;
52
+ fields.push({ field, wire, value: val });
53
+ }
54
+ else if (wire === 1) {
55
+ i += 8;
56
+ fields.push({ field, wire, value: 0 });
57
+ }
58
+ else if (wire === 5) {
59
+ i += 4;
60
+ fields.push({ field, wire, value: 0 });
61
+ }
62
+ else {
63
+ break;
64
+ }
65
+ }
66
+ return fields;
67
+ }
68
+ function fieldDict(body) {
69
+ const m = new Map();
70
+ for (const f of walkFields(body)) {
71
+ m.set(f.field, f.value);
72
+ }
73
+ return m;
74
+ }
75
+ function extractString(body, fieldNum) {
76
+ for (const f of walkFields(body)) {
77
+ if (f.field !== fieldNum || f.wire !== 2)
78
+ continue;
79
+ const val = f.value;
80
+ if (!(val instanceof Uint8Array))
81
+ continue;
82
+ try {
83
+ return new TextDecoder("utf-8", { fatal: true }).decode(val);
84
+ }
85
+ catch {
86
+ // try nested
87
+ for (const sf of walkFields(val)) {
88
+ if (sf.wire !== 2 || !(sf.value instanceof Uint8Array))
89
+ continue;
90
+ try {
91
+ return new TextDecoder("utf-8", { fatal: true }).decode(sf.value);
92
+ }
93
+ catch { }
94
+ }
95
+ }
96
+ }
97
+ return null;
98
+ }
99
+ function isSignatureFrame(body) {
100
+ const fd = fieldDict(body);
101
+ const f21 = fd.get(21);
102
+ if (f21 instanceof Uint8Array) {
103
+ try {
104
+ if (new TextDecoder().decode(f21) === "anthropic")
105
+ return true;
106
+ }
107
+ catch { }
108
+ }
109
+ return false;
110
+ }
111
+ function getStopReason(body) {
112
+ const f5 = fieldDict(body).get(5);
113
+ if (typeof f5 === "number")
114
+ return f5;
115
+ return null;
116
+ }
117
+ function extractToolCall(body) {
118
+ const f6 = fieldDict(body).get(6);
119
+ if (!(f6 instanceof Uint8Array))
120
+ return null;
121
+ const sf = fieldDict(f6);
122
+ const result = {};
123
+ const f1 = sf.get(1);
124
+ if (f1 instanceof Uint8Array) {
125
+ try {
126
+ result.id = new TextDecoder().decode(f1);
127
+ }
128
+ catch { }
129
+ }
130
+ const f2 = sf.get(2);
131
+ if (f2 instanceof Uint8Array) {
132
+ try {
133
+ result.name = new TextDecoder().decode(f2);
134
+ }
135
+ catch { }
136
+ }
137
+ const f3 = sf.get(3);
138
+ if (f3 instanceof Uint8Array) {
139
+ try {
140
+ result.argsChunk = new TextDecoder().decode(f3);
141
+ }
142
+ catch { }
143
+ }
144
+ if (result.id || result.name || result.argsChunk)
145
+ return result;
146
+ return null;
147
+ }
148
+ function extractStats(body) {
149
+ const fd = fieldDict(body);
150
+ const f7 = fd.get(7);
151
+ if (!(f7 instanceof Uint8Array))
152
+ return null;
153
+ const sf = fieldDict(f7);
154
+ const result = {};
155
+ const modelBytes = sf.get(9);
156
+ if (modelBytes instanceof Uint8Array) {
157
+ try {
158
+ result.model = new TextDecoder().decode(modelBytes);
159
+ }
160
+ catch { }
161
+ }
162
+ const inputF4 = sf.get(4);
163
+ const inputF3 = sf.get(3);
164
+ if (typeof inputF4 === "number") {
165
+ result.inputTokens = inputF4;
166
+ if (typeof inputF3 === "number")
167
+ result.cacheCreationInputTokens = inputF3;
168
+ }
169
+ else if (typeof inputF3 === "number") {
170
+ result.inputTokens = inputF3;
171
+ }
172
+ const outputTokens = sf.get(5);
173
+ if (typeof outputTokens === "number")
174
+ result.outputTokens = outputTokens;
175
+ const msgIdBytes = sf.get(7);
176
+ if (msgIdBytes instanceof Uint8Array) {
177
+ try {
178
+ result.msgId = new TextDecoder().decode(msgIdBytes);
179
+ }
180
+ catch { }
181
+ }
182
+ return Object.keys(result).length > 0 ? result : null;
183
+ }
184
+ function freshDecodeState() {
185
+ return { currentToolCallId: null, emittedFinish: false };
186
+ }
187
+ function mergeStats(state, stats) {
188
+ if (!stats)
189
+ return state;
190
+ const result = { ...state };
191
+ if (stats.model !== undefined)
192
+ result.model = stats.model;
193
+ if (stats.inputTokens !== undefined)
194
+ result.inputTokens = stats.inputTokens;
195
+ if (stats.outputTokens !== undefined)
196
+ result.outputTokens = stats.outputTokens;
197
+ if (stats.msgId !== undefined)
198
+ result.msgId = stats.msgId;
199
+ return result;
200
+ }
201
+ function buildFinishEvent(state, stopReason) {
202
+ return {
203
+ type: "finish",
204
+ model: state.model,
205
+ inputTokens: state.inputTokens,
206
+ outputTokens: state.outputTokens,
207
+ msgId: state.msgId,
208
+ stopReason,
209
+ };
210
+ }
211
+ function decodeFrame(body, state) {
212
+ // signature frame: skip (redacted thinking → answer boundary)
213
+ if (isSignatureFrame(body))
214
+ return { event: null, state };
215
+ // tool call sub-message f6
216
+ const toolCall = extractToolCall(body);
217
+ if (toolCall) {
218
+ // frame with f6.f1+f2 (id+name): start of a new tool call
219
+ if (toolCall.id) {
220
+ const newState = { ...state, currentToolCallId: toolCall.id };
221
+ if (toolCall.name) {
222
+ return { event: { type: "tool-call-start", id: toolCall.id, name: toolCall.name }, state: newState };
223
+ }
224
+ // id-only f6 frame: update tracking id, check for args chunk too
225
+ if (toolCall.argsChunk) {
226
+ return { event: { type: "tool-call-delta", id: toolCall.id, argsChunk: toolCall.argsChunk }, state: newState };
227
+ }
228
+ return { event: null, state: newState };
229
+ }
230
+ // frame with f6.f3 only (args delta): use the tracked current tool call id
231
+ if (toolCall.argsChunk) {
232
+ const id = state.currentToolCallId ?? "0";
233
+ return { event: { type: "tool-call-delta", id, argsChunk: toolCall.argsChunk }, state };
234
+ }
235
+ return { event: null, state };
236
+ }
237
+ // stop frame: any stop_reason from f5 (4=end_turn, 10=tool_use)
238
+ const stopReason = getStopReason(body);
239
+ if (stopReason !== null) {
240
+ // accumulate any stats in this frame too, then emit the single terminal finish
241
+ const stats = extractStats(body);
242
+ const finalState = mergeStats(mergeStats(state, stats), null);
243
+ return {
244
+ event: buildFinishEvent(finalState, stopReason),
245
+ state: { ...finalState, emittedFinish: true },
246
+ };
247
+ }
248
+ // text / reasoning / stats-only frames — accumulate stats silently, never emit finish
249
+ let newState = state;
250
+ const stats = extractStats(body);
251
+ if (stats)
252
+ newState = mergeStats(newState, stats);
253
+ const deltaThinking = extractString(body, 9);
254
+ if (deltaThinking !== null) {
255
+ return { event: { type: "reasoning", delta: deltaThinking }, state: newState };
256
+ }
257
+ const deltaText = extractString(body, 3);
258
+ if (deltaText !== null) {
259
+ return { event: { type: "text", delta: deltaText }, state: newState };
260
+ }
261
+ // stats-only frame (no delta, no stop, no tool-call) — already accumulated above
262
+ return { event: null, state: newState };
263
+ }
264
+ // ---------------------------------------------------------------------------
265
+ // Main streaming client
266
+ // ---------------------------------------------------------------------------
267
+ export async function* streamGetChatMessage(requestBody, signal) {
268
+ const resp = await fetch(ENDPOINT, {
269
+ method: "POST",
270
+ headers: {
271
+ "content-type": "application/connect+proto",
272
+ "connect-protocol-version": "1",
273
+ },
274
+ body: Buffer.from(requestBody),
275
+ signal,
276
+ });
277
+ if (!resp.ok) {
278
+ const errText = await resp.text().catch(() => "");
279
+ throw new Error(`GetChatMessage HTTP ${resp.status}: ${errText.slice(0, 500)}`);
280
+ }
281
+ const reader = resp.body?.getReader();
282
+ if (!reader)
283
+ throw new Error("Response body is not readable");
284
+ let buffer = new Uint8Array(0);
285
+ let decodeState = freshDecodeState();
286
+ try {
287
+ while (true) {
288
+ if (signal?.aborted)
289
+ break;
290
+ const { done, value } = await reader.read();
291
+ if (done)
292
+ break;
293
+ // Append to buffer
294
+ if (value) {
295
+ const newBuf = new Uint8Array(buffer.byteLength + value.byteLength);
296
+ newBuf.set(buffer);
297
+ newBuf.set(value, buffer.byteLength);
298
+ buffer = newBuf;
299
+ }
300
+ // Parse all complete frames from buffer
301
+ let remaining = buffer;
302
+ const frames = parseConnectFrames(remaining);
303
+ if (frames.length === 0)
304
+ continue;
305
+ // Calculate consumed bytes
306
+ let consumed = 0;
307
+ for (const frame of frames) {
308
+ consumed += 5 + frame.body.byteLength;
309
+ }
310
+ buffer = buffer.slice(consumed);
311
+ for (const frame of frames) {
312
+ if (frame.flag & CONNECT_FLAG_EOS)
313
+ continue; // skip EOS/trailer frame
314
+ const { event, state: newState } = decodeFrame(frame.body, decodeState);
315
+ decodeState = newState;
316
+ if (event)
317
+ yield event;
318
+ }
319
+ }
320
+ }
321
+ finally {
322
+ reader.releaseLock();
323
+ }
324
+ // Terminal — if the stream ended (EOS) without an explicit stop_reason frame,
325
+ // emit exactly one finish with accumulated stats.
326
+ if (!decodeState.emittedFinish) {
327
+ yield buildFinishEvent(decodeState);
328
+ }
329
+ }
@@ -0,0 +1,24 @@
1
+ export interface GetChatMessageInput {
2
+ jwt: string;
3
+ systemPrompt: string;
4
+ messages: Array<{
5
+ role: number;
6
+ content?: string;
7
+ toolCall?: {
8
+ id: string;
9
+ name: string;
10
+ argumentsJson: string;
11
+ };
12
+ toolResult?: {
13
+ toolCallId: string;
14
+ };
15
+ }>;
16
+ tools: Array<{
17
+ name: string;
18
+ description: string;
19
+ parametersJsonSchema: string;
20
+ }>;
21
+ modelId: string;
22
+ sessionId?: string;
23
+ }
24
+ export declare function encodeGetChatMessageRequest(input: GetChatMessageInput): Uint8Array;
@@ -0,0 +1,118 @@
1
+ // chat-request.ts — encode GetChatMessageRequest protobuf + Connect framing
2
+ //
3
+ // Field map from:
4
+ // opencode-windsurf-auth/research/GETCHATMESSAGE_REQUEST_PROTO.md
5
+ // opencode-windsurf-auth/research/decode_request.py
6
+ //
7
+ // Validation: the output of encodeGetChatMessageRequest can be round-trip
8
+ // decoded with decode_request.py to verify field counts and structure.
9
+ import { encodeConnectFrame, encodeFixed64Field, encodeMessageField, encodeStringField, encodeVarintField, } from "./proto";
10
+ function uuid() {
11
+ return crypto.randomUUID();
12
+ }
13
+ function concat(...arrays) {
14
+ const total = arrays.reduce((s, a) => s + a.byteLength, 0);
15
+ const result = new Uint8Array(total);
16
+ let offset = 0;
17
+ for (const a of arrays) {
18
+ result.set(a, offset);
19
+ offset += a.byteLength;
20
+ }
21
+ return result;
22
+ }
23
+ // ---------------------------------------------------------------------------
24
+ // Sub-message builders
25
+ // ---------------------------------------------------------------------------
26
+ /** Build the client_info sub-message (f1). */
27
+ function encodeClientInfo(jwt) {
28
+ return concat(encodeStringField(1, "chisel"), // f1: client_name
29
+ encodeStringField(2, "2026.5.26-2"), // f2: client_version
30
+ encodeStringField(3, `devin-session-token$${jwt}`), // f3: jwt_token
31
+ encodeStringField(4, "en"), // f4: locale
32
+ encodeStringField(5, "mac"), // f5: platform
33
+ encodeStringField(7, "2026.5.26-2"), // f7: client_version_2
34
+ encodeStringField(12, "chisel"));
35
+ }
36
+ /** Build the ChatToolCall sub-message used in f6 of role=2 messages and on the response side.
37
+ * Fields: f1=tool_call_id, f2=tool_name, f3=arguments_json. */
38
+ function encodeChatToolCall(tc) {
39
+ return concat(encodeStringField(1, tc.id), encodeStringField(2, tc.name), encodeStringField(3, tc.argumentsJson));
40
+ }
41
+ /** Build one message entry (f3 repeated).
42
+ * Role 1 (user): f1=msg_id, f2=1, f3=content_text
43
+ * Role 2 (assistant): f1=msg_id, f2=2, [f3=content_text if non-empty,] f6=ChatToolCall
44
+ * Role 4 (tool_result): f1=msg_id, f2=4, f3=content_text, f7=tool_call_id
45
+ * Default (role||1): same as role=1 for backward compatibility. */
46
+ function encodeMessage(m) {
47
+ const msgId = uuid();
48
+ const role = m.role || 1;
49
+ if (role === 2) {
50
+ const parts = [
51
+ encodeStringField(1, msgId),
52
+ encodeVarintField(2, 2),
53
+ ];
54
+ if (m.content)
55
+ parts.push(encodeStringField(3, m.content));
56
+ if (m.toolCall)
57
+ parts.push(encodeMessageField(6, encodeChatToolCall(m.toolCall)));
58
+ return concat(...parts);
59
+ }
60
+ if (role === 4) {
61
+ return concat(encodeStringField(1, msgId), encodeVarintField(2, 4), encodeStringField(3, m.content ?? ""), encodeStringField(7, m.toolResult?.toolCallId ?? ""));
62
+ }
63
+ // role=1 (user) — default path
64
+ return concat(encodeStringField(1, msgId), encodeVarintField(2, role), encodeStringField(3, m.content ?? ""));
65
+ }
66
+ /** Build one tool entry (f10 repeated). */
67
+ function encodeTool(name, description, parametersJsonSchema) {
68
+ return concat(encodeStringField(1, name), encodeStringField(2, description), encodeStringField(3, parametersJsonSchema));
69
+ }
70
+ /** Build the model_params sub-message (f8). */
71
+ function encodeModelParams() {
72
+ return concat(encodeVarintField(1, 1), // f1: unk1 (=1)
73
+ encodeVarintField(2, 128000), // f2: max_context_tokens
74
+ encodeVarintField(3, 400), // f3: max_output_tokens
75
+ encodeFixed64Field(5, 1.0), // f5: temperature
76
+ encodeVarintField(7, 40), // f7: top_k
77
+ encodeFixed64Field(8, 0.95));
78
+ }
79
+ /** Build the unk15 sub-message (f15).
80
+ *
81
+ * Matches the known-good title-gen capture shape: {f1:uuid, f2:1, f3:4}.
82
+ * The main-chat captures additionally carry f4 (a think-budget value) but the
83
+ * minimal title-gen request — proven to be accepted by the server — omits it.
84
+ * We mirror the minimal known-good shape to maximise acceptance. */
85
+ function encodeUnk15() {
86
+ return concat(encodeStringField(1, uuid()), // f1: uuid (per-request)
87
+ encodeVarintField(2, 1), // f2: unk2
88
+ encodeVarintField(3, 4));
89
+ }
90
+ // ---------------------------------------------------------------------------
91
+ // Main encoder
92
+ // ---------------------------------------------------------------------------
93
+ export function encodeGetChatMessageRequest(input) {
94
+ const sessionId = input.sessionId ?? uuid();
95
+ const body = concat(
96
+ // f1: client_info
97
+ encodeMessageField(1, encodeClientInfo(input.jwt)),
98
+ // f2: system_prompt
99
+ encodeStringField(2, input.systemPrompt),
100
+ // f3: messages[] (repeated). Role 1=user, 2=assistant (with optional
101
+ // ChatToolCall in f6), 4=tool_result (with tool_call_id in f7).
102
+ ...input.messages.map((m) => encodeMessageField(3, encodeMessage(m))),
103
+ // f7: unk7 = 5
104
+ encodeVarintField(7, 5),
105
+ // f8: model_params
106
+ encodeMessageField(8, encodeModelParams()),
107
+ // f10: tools[] (repeated)
108
+ ...input.tools.map((t) => encodeMessageField(10, encodeTool(t.name, t.description, t.parametersJsonSchema))),
109
+ // f15: unk15
110
+ encodeMessageField(15, encodeUnk15()),
111
+ // f16: session_id
112
+ encodeStringField(16, sessionId),
113
+ // f20: unk20 = 1
114
+ encodeVarintField(20, 1),
115
+ // f21: model_id
116
+ encodeStringField(21, input.modelId));
117
+ return encodeConnectFrame(body);
118
+ }
@@ -0,0 +1 @@
1
+ export declare function loadWindsurfJwt(): Promise<string | null>;