Skip to content

Commit 7e6bbe9

Browse files
Serhii DziupinSerhii Dziupin
andcommitted
fix: align gateway body with Command Code schema; pass live Laguna
Correct /alpha/generate payload (structure[] + mode=agent), harden live tests for streamed deltas, and verify Go-plan session end-to-end on poolside/laguna-s-2.1-free via proxy and opencode run. Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
1 parent b39cd64 commit 7e6bbe9

3 files changed

Lines changed: 90 additions & 22 deletions

File tree

‎src/gateway-types.ts‎

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,13 +57,33 @@ export type WireUsage = {
5757
};
5858

5959
export type GenerateBody = {
60-
config: Record<string, unknown>;
60+
config: {
61+
workingDir: string;
62+
date: string;
63+
environment: string;
64+
structure: string[];
65+
isGitRepo: boolean;
66+
currentBranch: string;
67+
mainBranch: string;
68+
gitStatus: string;
69+
recentCommits: string[];
70+
[key: string]: unknown;
71+
};
6172
memory: null;
6273
taste: null;
6374
skills: null;
6475
permissionMode: "standard" | "auto-accept" | "plan";
6576
threadId?: string;
66-
mode?: string;
77+
mode?:
78+
| "agent"
79+
| "learning"
80+
| "custom-agent"
81+
| "custom-agent-create"
82+
| "title-gen"
83+
| "tool-desc"
84+
| "compact"
85+
| "vision"
86+
| string;
6787
params: {
6888
model: string;
6989
messages: WireMessage[];

‎src/gateway.ts‎

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
* Command Code gateway client — POST /alpha/generate NDJSON stream.
33
*/
44
import { createHash, randomUUID } from "node:crypto";
5+
import { readdirSync } from "node:fs";
56
import {
67
GENERATE_ROUTE,
78
emptyUsage,
@@ -83,12 +84,23 @@ export function buildAuthHeaders(auth: GatewayAuthHeaders): Record<string, strin
8384
export function buildGenerateBody(
8485
params: GatewayGenerateParams,
8586
): GenerateBody {
87+
const cwd = process.env.OPENCODE_COMMANDCODE_CWD || process.cwd();
88+
let structure: string[] = [];
89+
try {
90+
structure = readdirSync(cwd)
91+
.filter((name) => !name.startsWith("."))
92+
.sort()
93+
.slice(0, 200);
94+
} catch {
95+
structure = [];
96+
}
97+
8698
return {
8799
config: {
88-
workingDir: process.env.OPENCODE_COMMANDCODE_CWD || process.cwd(),
100+
workingDir: cwd,
89101
date: new Date().toISOString().slice(0, 10),
90102
environment: process.platform,
91-
structure: "",
103+
structure,
92104
isGitRepo: false,
93105
currentBranch: "",
94106
mainBranch: "",
@@ -100,7 +112,8 @@ export function buildGenerateBody(
100112
skills: null,
101113
permissionMode: params.permissionMode || "auto-accept",
102114
...(params.threadId ? { threadId: params.threadId } : {}),
103-
mode: "default",
115+
// Feature mode — must be a gateway feature id, not "default".
116+
mode: "agent",
104117
params: {
105118
model: params.model,
106119
messages: params.messages,

‎test/live-laguna.ts‎

Lines changed: 52 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,45 @@ function requireAuth(): string {
2929
return creds.apiKey;
3030
}
3131

32-
async function readSseText(res: Response): Promise<string> {
33-
return res.text();
32+
33+
async function assertOk(res: Response, label: string): Promise<string> {
34+
const text = await res.text();
35+
if (res.status !== 200) {
36+
throw new Error(`${label}: expected 200, got ${res.status}\n${text.slice(0, 2000)}`);
37+
}
38+
return text;
39+
}
40+
41+
/** Concatenate streamed text/reasoning deltas from an OpenAI SSE body. */
42+
function sseAssistantText(sse: string): string {
43+
let out = "";
44+
for (const line of sse.split("\n")) {
45+
if (!line.startsWith("data: ")) continue;
46+
const raw = line.slice(6).trim();
47+
if (!raw || raw === "[DONE]") continue;
48+
try {
49+
const chunk = JSON.parse(raw) as {
50+
choices?: Array<{
51+
delta?: { content?: string; reasoning_content?: string };
52+
message?: { content?: string; reasoning_content?: string };
53+
}>;
54+
};
55+
const choice = chunk.choices?.[0];
56+
const delta = choice?.delta;
57+
const message = choice?.message;
58+
if (typeof delta?.content === "string") out += delta.content;
59+
if (typeof delta?.reasoning_content === "string") {
60+
out += delta.reasoning_content;
61+
}
62+
if (typeof message?.content === "string") out += message.content;
63+
if (typeof message?.reasoning_content === "string") {
64+
out += message.reasoning_content;
65+
}
66+
} catch {
67+
// ignore non-JSON keepalives
68+
}
69+
}
70+
return out;
3471
}
3572

3673
async function main() {
@@ -63,9 +100,9 @@ async function main() {
63100
],
64101
}),
65102
});
66-
assert.equal(res.status, 200, await res.text());
67-
const text = await readSseText(res);
68-
assert.ok(/Laguna live ok/i.test(text), text.slice(0, 2000));
103+
const text = await assertOk(res, "plain");
104+
const assistant = sseAssistantText(text);
105+
assert.ok(/Laguna live ok/i.test(assistant), assistant || text.slice(0, 2000));
69106
assert.ok(text.includes("usage"), "expected usage in stream");
70107
console.log("✓ live plain completion");
71108
}
@@ -108,8 +145,8 @@ async function main() {
108145
],
109146
}),
110147
});
111-
assert.equal(res.status, 200, await res.text());
112-
const json = (await res.json()) as {
148+
const raw = await assertOk(res, "attachments");
149+
const json = JSON.parse(raw) as {
113150
choices: Array<{ message: { content: string } }>;
114151
usage?: { prompt_tokens: number };
115152
};
@@ -159,8 +196,7 @@ async function main() {
159196
tools,
160197
}),
161198
});
162-
assert.equal(res1.status, 200, await res1.text());
163-
const turn1 = await readSseText(res1);
199+
const turn1 = await assertOk(res1, "tools-turn1");
164200
assert.ok(
165201
turn1.includes("add_numbers") || turn1.includes("tool_calls"),
166202
turn1.slice(0, 2500),
@@ -202,7 +238,6 @@ async function main() {
202238
} catch {
203239
parsed = {};
204240
}
205-
// args may have been streamed in pieces — try to find numbers in the SSE
206241
if (typeof parsed.a !== "number") {
207242
const m = turn1.match(/"a"\s*:\s*(\d+)/);
208243
const n = turn1.match(/"b"\s*:\s*(\d+)/);
@@ -254,18 +289,18 @@ async function main() {
254289
tools,
255290
}),
256291
});
257-
assert.equal(res2.status, 200, await res2.text());
258-
const turn2 = await readSseText(res2);
259-
assert.ok(/42/.test(turn2), turn2.slice(0, 2000));
292+
const turn2 = await assertOk(res2, "tools-turn2");
293+
const assistant2 = sseAssistantText(turn2);
294+
assert.ok(/42/.test(assistant2) || /42/.test(turn2), assistant2 || turn2.slice(0, 2000));
260295
console.log("✓ live tools park/resume");
261296
}
262297
}
263298

264299
// 4) Usage totals
265300
{
266301
const res = await fetch(`${base}/v1/usage`);
267-
assert.equal(res.status, 200);
268-
const json = (await res.json()) as {
302+
const raw = await assertOk(res, "usage");
303+
const json = JSON.parse(raw) as {
269304
total: { prompt_tokens: number; completion_tokens: number };
270305
sessions: Array<{ tools: unknown[]; contextWindow: number }>;
271306
};
@@ -295,8 +330,8 @@ async function main() {
295330
],
296331
}),
297332
});
298-
assert.equal(res.status, 200, await res.text());
299-
const json = (await res.json()) as {
333+
const raw = await assertOk(res, "compact");
334+
const json = JSON.parse(raw) as {
300335
choices: Array<{ message: { content?: string; reasoning_content?: string } }>;
301336
};
302337
const content =

0 commit comments

Comments
 (0)