Skip to content

Commit 76caa4a

Browse files
committed
fix: migrate Gemini Interactions non-streaming builders to SDK 2.x steps
The streamed SSE emitter was migrated to SDK 2.x in this PR, but the non-streaming (unary) JSON response builders still emitted the SDK 1.x { ..., outputs: [...] } envelope. The @google/genai v2 Interactions consumer reads non-streaming content from interaction.steps (model_output content[] for text, function_call steps for tools) with an output_text fast-path — never from outputs — so a v2 consumer doing a non-streaming completion got a silently empty result, the same failure class the streaming half already fixed. - buildInteractionsTextResponse: emit output_text + a single model_output step wrapping the text part. - buildInteractionsToolCallResponse: emit function_call steps (id/name/ parsed arguments) instead of outputs; preserve the malformed-args try/catch + logger.warn guard via a shared buildFunctionCallStep helper. - buildInteractionsContentWithToolCallsResponse: emit output_text + a model_output text step followed by function_call steps. - Flip the drift SDK shapes and the non-streaming unit/integration tests to assert the v2 steps/output_text shape (red-green proven).
1 parent 24de47f commit 76caa4a

3 files changed

Lines changed: 76 additions & 70 deletions

File tree

src/__tests__/drift/sdk-shapes.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1587,7 +1587,8 @@ export function geminiInteractionsResponseShape(): ShapeNode {
15871587
status: "completed",
15881588
model: "gemini-2.5-flash",
15891589
role: "model",
1590-
outputs: [{ type: "text", text: "Hello!" }],
1590+
output_text: "Hello!",
1591+
steps: [{ type: "model_output", content: [{ type: "text", text: "Hello!" }] }],
15911592
usage: { total_input_tokens: 0, total_output_tokens: 0, total_tokens: 0 },
15921593
});
15931594
}
@@ -1598,7 +1599,7 @@ export function geminiInteractionsToolCallResponseShape(): ShapeNode {
15981599
status: "requires_action",
15991600
model: "gemini-2.5-flash",
16001601
role: "model",
1601-
outputs: [
1602+
steps: [
16021603
{
16031604
type: "function_call",
16041605
id: "call_abc123",

src/__tests__/gemini-interactions.test.ts

Lines changed: 48 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -745,7 +745,10 @@ describe("response builders", () => {
745745
expect(resp.status).toBe("completed");
746746
expect(resp.model).toBe("gemini-2.5-flash");
747747
expect(resp.role).toBe("model");
748-
expect(resp.outputs).toEqual([{ type: "text", text: "Hello!" }]);
748+
expect(resp.output_text).toBe("Hello!");
749+
expect(resp.steps).toEqual([
750+
{ type: "model_output", content: [{ type: "text", text: "Hello!" }] },
751+
]);
749752
});
750753

751754
it("builds tool call response", () => {
@@ -756,11 +759,12 @@ describe("response builders", () => {
756759
logger,
757760
) as Record<string, unknown>;
758761
expect(resp.status).toBe("requires_action");
759-
const outputs = resp.outputs as Array<Record<string, unknown>>;
760-
expect(outputs).toHaveLength(1);
761-
expect(outputs[0].type).toBe("function_call");
762-
expect(outputs[0].name).toBe("get_weather");
763-
expect(outputs[0].arguments).toEqual({ city: "NYC" });
762+
const steps = resp.steps as Array<Record<string, unknown>>;
763+
expect(steps).toHaveLength(1);
764+
expect(steps[0].type).toBe("function_call");
765+
expect(steps[0].id).toBe("call_1");
766+
expect(steps[0].name).toBe("get_weather");
767+
expect(steps[0].arguments).toEqual({ city: "NYC" });
764768
});
765769

766770
it("builds content+tools response", () => {
@@ -772,10 +776,13 @@ describe("response builders", () => {
772776
logger,
773777
) as Record<string, unknown>;
774778
expect(resp.status).toBe("requires_action");
775-
const outputs = resp.outputs as Array<Record<string, unknown>>;
776-
expect(outputs).toHaveLength(2);
777-
expect(outputs[0].type).toBe("text");
778-
expect(outputs[1].type).toBe("function_call");
779+
expect(resp.output_text).toBe("Here is the analysis");
780+
const steps = resp.steps as Array<Record<string, unknown>>;
781+
expect(steps).toHaveLength(2);
782+
expect(steps[0].type).toBe("model_output");
783+
expect(steps[0].content).toEqual([{ type: "text", text: "Here is the analysis" }]);
784+
expect(steps[1].type).toBe("function_call");
785+
expect(steps[1].name).toBe("analyze");
779786
});
780787

781788
it("includes usage metadata", () => {
@@ -816,8 +823,8 @@ describe("response builders", () => {
816823
"id-0",
817824
logger,
818825
) as Record<string, unknown>;
819-
const outputs = resp.outputs as Array<Record<string, unknown>>;
820-
expect(outputs[0].arguments).toEqual({});
826+
const steps = resp.steps as Array<Record<string, unknown>>;
827+
expect(steps[0].arguments).toEqual({});
821828
});
822829
});
823830

@@ -1010,7 +1017,10 @@ describe("Gemini Interactions — non-streaming", () => {
10101017
const body = JSON.parse(res.body);
10111018
expect(body.status).toBe("completed");
10121019
expect(body.role).toBe("model");
1013-
expect(body.outputs).toEqual([{ type: "text", text: "Hi there!" }]);
1020+
expect(body.output_text).toBe("Hi there!");
1021+
expect(body.steps).toEqual([
1022+
{ type: "model_output", content: [{ type: "text", text: "Hi there!" }] },
1023+
]);
10141024
expect(body.id).toMatch(/^aimock-int-/);
10151025
});
10161026

@@ -1024,11 +1034,11 @@ describe("Gemini Interactions — non-streaming", () => {
10241034
expect(res.status).toBe(200);
10251035
const body = JSON.parse(res.body);
10261036
expect(body.status).toBe("requires_action");
1027-
const outputs = body.outputs;
1028-
expect(outputs).toHaveLength(1);
1029-
expect(outputs[0].type).toBe("function_call");
1030-
expect(outputs[0].name).toBe("get_weather");
1031-
expect(outputs[0].arguments).toEqual({ city: "NYC" });
1037+
const steps = body.steps;
1038+
expect(steps).toHaveLength(1);
1039+
expect(steps[0].type).toBe("function_call");
1040+
expect(steps[0].name).toBe("get_weather");
1041+
expect(steps[0].arguments).toEqual({ city: "NYC" });
10321042
});
10331043

10341044
it("returns content + tool calls response", async () => {
@@ -1041,11 +1051,12 @@ describe("Gemini Interactions — non-streaming", () => {
10411051
expect(res.status).toBe(200);
10421052
const body = JSON.parse(res.body);
10431053
expect(body.status).toBe("requires_action");
1044-
expect(body.outputs).toHaveLength(2);
1045-
expect(body.outputs[0].type).toBe("text");
1046-
expect(body.outputs[0].text).toBe("Let me help you");
1047-
expect(body.outputs[1].type).toBe("function_call");
1048-
expect(body.outputs[1].name).toBe("analyze_data");
1054+
expect(body.output_text).toBe("Let me help you");
1055+
expect(body.steps).toHaveLength(2);
1056+
expect(body.steps[0].type).toBe("model_output");
1057+
expect(body.steps[0].content[0].text).toBe("Let me help you");
1058+
expect(body.steps[1].type).toBe("function_call");
1059+
expect(body.steps[1].name).toBe("analyze_data");
10491060
});
10501061

10511062
it("returns error response", async () => {
@@ -1122,7 +1133,10 @@ describe("Gemini Interactions — non-streaming", () => {
11221133
});
11231134
expect(res.status).toBe(200);
11241135
const body = JSON.parse(res.body);
1125-
expect(body.outputs).toEqual([{ type: "text", text: "Hi there!" }]);
1136+
expect(body.output_text).toBe("Hi there!");
1137+
expect(body.steps).toEqual([
1138+
{ type: "model_output", content: [{ type: "text", text: "Hi there!" }] },
1139+
]);
11261140
});
11271141

11281142
it("handles sequenceIndex for multi-turn", async () => {
@@ -1137,8 +1151,8 @@ describe("Gemini Interactions — non-streaming", () => {
11371151
input: "step",
11381152
stream: false,
11391153
});
1140-
expect(JSON.parse(r1.body).outputs[0].text).toBe("First");
1141-
expect(JSON.parse(r2.body).outputs[0].text).toBe("Second");
1154+
expect(JSON.parse(r1.body).output_text).toBe("First");
1155+
expect(JSON.parse(r2.body).output_text).toBe("Second");
11421156
});
11431157
});
11441158

@@ -1334,7 +1348,7 @@ describe("Gemini Interactions — fixture matching", () => {
13341348
input: "hello",
13351349
stream: false,
13361350
});
1337-
expect(JSON.parse(res.body).outputs[0].text).toBe("Hi there!");
1351+
expect(JSON.parse(res.body).output_text).toBe("Hi there!");
13381352
});
13391353

13401354
it("matches by sequenceIndex chaining", async () => {
@@ -1349,8 +1363,8 @@ describe("Gemini Interactions — fixture matching", () => {
13491363
input: "step",
13501364
stream: false,
13511365
});
1352-
expect(JSON.parse(r1.body).outputs[0].text).toBe("First");
1353-
expect(JSON.parse(r2.body).outputs[0].text).toBe("Second");
1366+
expect(JSON.parse(r1.body).output_text).toBe("First");
1367+
expect(JSON.parse(r2.body).output_text).toBe("Second");
13541368
});
13551369

13561370
it("matches by model", async () => {
@@ -1360,7 +1374,7 @@ describe("Gemini Interactions — fixture matching", () => {
13601374
input: "anything",
13611375
stream: false,
13621376
});
1363-
expect(JSON.parse(res.body).outputs[0].text).toBe("Pro response");
1377+
expect(JSON.parse(res.body).output_text).toBe("Pro response");
13641378
});
13651379

13661380
it("matches by predicate", async () => {
@@ -1370,7 +1384,7 @@ describe("Gemini Interactions — fixture matching", () => {
13701384
input: "custom-check",
13711385
stream: false,
13721386
});
1373-
expect(JSON.parse(res.body).outputs[0].text).toBe("Predicate matched");
1387+
expect(JSON.parse(res.body).output_text).toBe("Predicate matched");
13741388
});
13751389

13761390
it("matches by toolName for tool-related fixtures", async () => {
@@ -1394,7 +1408,7 @@ describe("Gemini Interactions — fixture matching", () => {
13941408
});
13951409
expect(res.status).toBe(200);
13961410
const body = JSON.parse(res.body);
1397-
expect(body.outputs[0].name).toBe("search_tool");
1411+
expect(body.steps[0].name).toBe("search_tool");
13981412
});
13991413
});
14001414

@@ -1677,7 +1691,8 @@ describe("Gemini Interactions — edge cases", () => {
16771691
});
16781692
expect(res.status).toBe(200);
16791693
const body = JSON.parse(res.body);
1680-
expect(body.outputs[0].text).toBe("");
1694+
expect(body.output_text).toBe("");
1695+
expect(body.steps[0].content[0].text).toBe("");
16811696
});
16821697

16831698
it("streams empty content correctly", async () => {

src/gemini-interactions.ts

Lines changed: 25 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -337,11 +337,30 @@ export function buildInteractionsTextResponse(
337337
status: "completed",
338338
model: overrides?.model ?? model,
339339
role: "model",
340-
outputs: [{ type: "text", text: content }],
340+
output_text: content,
341+
steps: [{ type: "model_output", content: [{ type: "text", text: content }] }],
341342
usage: interactionsUsage(overrides),
342343
};
343344
}
344345

346+
// Build a single SDK 2.x function_call step from a fixture tool call,
347+
// reusing the existing malformed-arguments guard (logger.warn + {} fallback).
348+
function buildFunctionCallStep(tc: ToolCall, logger: Logger): object {
349+
let argsObj: unknown;
350+
try {
351+
argsObj = JSON.parse(tc.arguments || "{}");
352+
} catch {
353+
logger.warn(`Malformed JSON in fixture tool call arguments for "${tc.name}": ${tc.arguments}`);
354+
argsObj = {};
355+
}
356+
return {
357+
type: "function_call",
358+
id: tc.id || generateToolCallId(),
359+
name: tc.name,
360+
arguments: argsObj,
361+
};
362+
}
363+
345364
export function buildInteractionsToolCallResponse(
346365
toolCalls: ToolCall[],
347366
model: string,
@@ -354,23 +373,7 @@ export function buildInteractionsToolCallResponse(
354373
status: "requires_action",
355374
model: overrides?.model ?? model,
356375
role: "model",
357-
outputs: toolCalls.map((tc) => {
358-
let argsObj: unknown;
359-
try {
360-
argsObj = JSON.parse(tc.arguments || "{}");
361-
} catch {
362-
logger.warn(
363-
`Malformed JSON in fixture tool call arguments for "${tc.name}": ${tc.arguments}`,
364-
);
365-
argsObj = {};
366-
}
367-
return {
368-
type: "function_call",
369-
id: tc.id || generateToolCallId(),
370-
name: tc.name,
371-
arguments: argsObj,
372-
};
373-
}),
376+
steps: toolCalls.map((tc) => buildFunctionCallStep(tc, logger)),
374377
usage: interactionsUsage(overrides),
375378
};
376379
}
@@ -383,31 +386,18 @@ export function buildInteractionsContentWithToolCallsResponse(
383386
logger: Logger,
384387
overrides?: ResponseOverrides,
385388
): object {
386-
const outputs: object[] = [{ type: "text", text: content }];
389+
const steps: object[] = [{ type: "model_output", content: [{ type: "text", text: content }] }];
387390
for (const tc of toolCalls) {
388-
let argsObj: unknown;
389-
try {
390-
argsObj = JSON.parse(tc.arguments || "{}");
391-
} catch {
392-
logger.warn(
393-
`Malformed JSON in fixture tool call arguments for "${tc.name}": ${tc.arguments}`,
394-
);
395-
argsObj = {};
396-
}
397-
outputs.push({
398-
type: "function_call",
399-
id: tc.id || generateToolCallId(),
400-
name: tc.name,
401-
arguments: argsObj,
402-
});
391+
steps.push(buildFunctionCallStep(tc, logger));
403392
}
404393

405394
return {
406395
id: interactionId,
407396
status: "requires_action",
408397
model: overrides?.model ?? model,
409398
role: "model",
410-
outputs,
399+
output_text: content,
400+
steps,
411401
usage: interactionsUsage(overrides),
412402
};
413403
}

0 commit comments

Comments
 (0)