Format messages so they work with Gemini API (#266)
* fix: Gemini stops working after one message in a session * fix: small issue in test file * test: cover google role-merge behavior --------- Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
@@ -15,6 +15,7 @@
|
|||||||
- Auth: lock auth profile refreshes to avoid multi-instance OAuth logouts; keep credentials on refresh failure.
|
- Auth: lock auth profile refreshes to avoid multi-instance OAuth logouts; keep credentials on refresh failure.
|
||||||
- Onboarding: prompt immediately for OpenAI Codex redirect URL on remote/headless logins.
|
- Onboarding: prompt immediately for OpenAI Codex redirect URL on remote/headless logins.
|
||||||
- Typing indicators: stop typing once the reply dispatcher drains to prevent stuck typing across Discord/Telegram/WhatsApp.
|
- Typing indicators: stop typing once the reply dispatcher drains to prevent stuck typing across Discord/Telegram/WhatsApp.
|
||||||
|
- Google: merge consecutive messages to satisfy strict role alternation for Google provider models. Thanks @Asleep123 for PR #266.
|
||||||
- WhatsApp/Telegram: add groupPolicy handling for group messages and normalize allowFrom matching (tg/telegram prefixes). Thanks @mneves75.
|
- WhatsApp/Telegram: add groupPolicy handling for group messages and normalize allowFrom matching (tg/telegram prefixes). Thanks @mneves75.
|
||||||
- Auto-reply: add configurable ack reactions for inbound messages (default 👀 or `identity.emoji`) with scope controls. Thanks @obviyus for PR #178.
|
- Auto-reply: add configurable ack reactions for inbound messages (default 👀 or `identity.emoji`) with scope controls. Thanks @obviyus for PR #178.
|
||||||
- Onboarding: resolve CLI entrypoint when running via `npx` so gateway daemon install works without a build step.
|
- Onboarding: resolve CLI entrypoint when running via `npx` so gateway daemon install works without a build step.
|
||||||
|
|||||||
@@ -1,8 +1,52 @@
|
|||||||
diff --git a/dist/providers/google-shared.js b/dist/providers/google-shared.js
|
diff --git a/dist/providers/google-shared.js b/dist/providers/google-shared.js
|
||||||
index 7bc0a9f5d6241f191cd607ecb37b3acac8d58267..76166a34784cbc0718d4b9bd1fa6336a6dd394ec 100644
|
index 7bc0a9f5d6241f191cd607ecb37b3acac8d58267..56866774e47444b5d333961c9b20fce582363124 100644
|
||||||
--- a/dist/providers/google-shared.js
|
--- a/dist/providers/google-shared.js
|
||||||
+++ b/dist/providers/google-shared.js
|
+++ b/dist/providers/google-shared.js
|
||||||
@@ -51,9 +51,19 @@ export function convertMessages(model, context) {
|
@@ -10,13 +10,27 @@ import { transformMessages } from "./transorm-messages.js";
|
||||||
|
export function convertMessages(model, context) {
|
||||||
|
const contents = [];
|
||||||
|
const transformedMessages = transformMessages(context.messages, model);
|
||||||
|
+
|
||||||
|
+ /**
|
||||||
|
+ * Helper to add content while merging consecutive messages of the same role.
|
||||||
|
+ * Gemini/Cloud Code Assist requires strict role alternation (user/model/user/model).
|
||||||
|
+ * Consecutive messages of the same role cause "function call turn" errors.
|
||||||
|
+ */
|
||||||
|
+ function addContent(role, parts) {
|
||||||
|
+ if (parts.length === 0) return;
|
||||||
|
+ const lastContent = contents[contents.length - 1];
|
||||||
|
+ if (lastContent?.role === role) {
|
||||||
|
+ // Merge into existing message of same role
|
||||||
|
+ lastContent.parts.push(...parts);
|
||||||
|
+ } else {
|
||||||
|
+ contents.push({ role, parts });
|
||||||
|
+ }
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
for (const msg of transformedMessages) {
|
||||||
|
if (msg.role === "user") {
|
||||||
|
if (typeof msg.content === "string") {
|
||||||
|
- contents.push({
|
||||||
|
- role: "user",
|
||||||
|
- parts: [{ text: sanitizeSurrogates(msg.content) }],
|
||||||
|
- });
|
||||||
|
+ addContent("user", [{ text: sanitizeSurrogates(msg.content) }]);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const parts = msg.content.map((item) => {
|
||||||
|
@@ -35,10 +49,7 @@ export function convertMessages(model, context) {
|
||||||
|
const filteredParts = !model.input.includes("image") ? parts.filter((p) => p.text !== undefined) : parts;
|
||||||
|
if (filteredParts.length === 0)
|
||||||
|
continue;
|
||||||
|
- contents.push({
|
||||||
|
- role: "user",
|
||||||
|
- parts: filteredParts,
|
||||||
|
- });
|
||||||
|
+ addContent("user", filteredParts);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (msg.role === "assistant") {
|
||||||
|
@@ -51,9 +62,19 @@ export function convertMessages(model, context) {
|
||||||
parts.push({ text: sanitizeSurrogates(block.text) });
|
parts.push({ text: sanitizeSurrogates(block.text) });
|
||||||
}
|
}
|
||||||
else if (block.type === "thinking") {
|
else if (block.type === "thinking") {
|
||||||
@@ -25,7 +69,7 @@ index 7bc0a9f5d6241f191cd607ecb37b3acac8d58267..76166a34784cbc0718d4b9bd1fa6336a
|
|||||||
parts.push({
|
parts.push({
|
||||||
thought: true,
|
thought: true,
|
||||||
text: sanitizeSurrogates(block.thinking),
|
text: sanitizeSurrogates(block.thinking),
|
||||||
@@ -61,6 +71,7 @@ export function convertMessages(model, context) {
|
@@ -61,6 +82,7 @@ export function convertMessages(model, context) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -33,7 +77,44 @@ index 7bc0a9f5d6241f191cd607ecb37b3acac8d58267..76166a34784cbc0718d4b9bd1fa6336a
|
|||||||
parts.push({
|
parts.push({
|
||||||
text: `<thinking>\n${sanitizeSurrogates(block.thinking)}\n</thinking>`,
|
text: `<thinking>\n${sanitizeSurrogates(block.thinking)}\n</thinking>`,
|
||||||
});
|
});
|
||||||
@@ -146,6 +157,77 @@ export function convertMessages(model, context) {
|
@@ -85,10 +107,7 @@ export function convertMessages(model, context) {
|
||||||
|
}
|
||||||
|
if (parts.length === 0)
|
||||||
|
continue;
|
||||||
|
- contents.push({
|
||||||
|
- role: "model",
|
||||||
|
- parts,
|
||||||
|
- });
|
||||||
|
+ addContent("model", parts);
|
||||||
|
}
|
||||||
|
else if (msg.role === "toolResult") {
|
||||||
|
// Extract text and image content
|
||||||
|
@@ -125,27 +144,94 @@ export function convertMessages(model, context) {
|
||||||
|
}
|
||||||
|
// Cloud Code Assist API requires all function responses to be in a single user turn.
|
||||||
|
// Check if the last content is already a user turn with function responses and merge.
|
||||||
|
+ // Use addContent for proper role alternation handling.
|
||||||
|
const lastContent = contents[contents.length - 1];
|
||||||
|
if (lastContent?.role === "user" && lastContent.parts?.some((p) => p.functionResponse)) {
|
||||||
|
lastContent.parts.push(functionResponsePart);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
- contents.push({
|
||||||
|
- role: "user",
|
||||||
|
- parts: [functionResponsePart],
|
||||||
|
- });
|
||||||
|
+ addContent("user", [functionResponsePart]);
|
||||||
|
}
|
||||||
|
// For older models, add images in a separate user message
|
||||||
|
+ // Note: This may create consecutive user messages, but addContent will merge them
|
||||||
|
if (hasImages && !supportsMultimodalFunctionResponse) {
|
||||||
|
- contents.push({
|
||||||
|
- role: "user",
|
||||||
|
- parts: [{ text: "Tool result image:" }, ...imageParts],
|
||||||
|
- });
|
||||||
|
+ addContent("user", [{ text: "Tool result image:" }, ...imageParts]);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return contents;
|
return contents;
|
||||||
}
|
}
|
||||||
@@ -111,7 +192,7 @@ index 7bc0a9f5d6241f191cd607ecb37b3acac8d58267..76166a34784cbc0718d4b9bd1fa6336a
|
|||||||
/**
|
/**
|
||||||
* Convert tools to Gemini function declarations format.
|
* Convert tools to Gemini function declarations format.
|
||||||
*/
|
*/
|
||||||
@@ -157,7 +239,7 @@ export function convertTools(tools) {
|
@@ -157,7 +243,7 @@ export function convertTools(tools) {
|
||||||
functionDeclarations: tools.map((tool) => ({
|
functionDeclarations: tools.map((tool) => ({
|
||||||
name: tool.name,
|
name: tool.name,
|
||||||
description: tool.description,
|
description: tool.description,
|
||||||
|
|||||||
10
pnpm-lock.yaml
generated
10
pnpm-lock.yaml
generated
@@ -9,7 +9,7 @@ overrides:
|
|||||||
|
|
||||||
patchedDependencies:
|
patchedDependencies:
|
||||||
'@mariozechner/pi-ai':
|
'@mariozechner/pi-ai':
|
||||||
hash: 628fb051b6f4886984a846a5ee7aa0a571c3360d35b8d114e4684e5edcd100c5
|
hash: b49275c3e2023970d8248ababef6df60e093e58a3ba3127c2ba4de1df387d06a
|
||||||
path: patches/@mariozechner__pi-ai.patch
|
path: patches/@mariozechner__pi-ai.patch
|
||||||
qrcode-terminal:
|
qrcode-terminal:
|
||||||
hash: ed82029850dbdf551f5df1de320945af52b8ea8500cc7bd4f39258e7a3d92e12
|
hash: ed82029850dbdf551f5df1de320945af52b8ea8500cc7bd4f39258e7a3d92e12
|
||||||
@@ -33,7 +33,7 @@ importers:
|
|||||||
version: 0.37.2(ws@8.19.0)(zod@4.3.5)
|
version: 0.37.2(ws@8.19.0)(zod@4.3.5)
|
||||||
'@mariozechner/pi-ai':
|
'@mariozechner/pi-ai':
|
||||||
specifier: ^0.37.2
|
specifier: ^0.37.2
|
||||||
version: 0.37.2(patch_hash=628fb051b6f4886984a846a5ee7aa0a571c3360d35b8d114e4684e5edcd100c5)(ws@8.19.0)(zod@4.3.5)
|
version: 0.37.2(patch_hash=b49275c3e2023970d8248ababef6df60e093e58a3ba3127c2ba4de1df387d06a)(ws@8.19.0)(zod@4.3.5)
|
||||||
'@mariozechner/pi-coding-agent':
|
'@mariozechner/pi-coding-agent':
|
||||||
specifier: ^0.37.2
|
specifier: ^0.37.2
|
||||||
version: 0.37.2(ws@8.19.0)(zod@4.3.5)
|
version: 0.37.2(ws@8.19.0)(zod@4.3.5)
|
||||||
@@ -3602,7 +3602,7 @@ snapshots:
|
|||||||
|
|
||||||
'@mariozechner/pi-agent-core@0.37.2(ws@8.19.0)(zod@4.3.5)':
|
'@mariozechner/pi-agent-core@0.37.2(ws@8.19.0)(zod@4.3.5)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@mariozechner/pi-ai': 0.37.2(patch_hash=628fb051b6f4886984a846a5ee7aa0a571c3360d35b8d114e4684e5edcd100c5)(ws@8.19.0)(zod@4.3.5)
|
'@mariozechner/pi-ai': 0.37.2(patch_hash=b49275c3e2023970d8248ababef6df60e093e58a3ba3127c2ba4de1df387d06a)(ws@8.19.0)(zod@4.3.5)
|
||||||
'@mariozechner/pi-tui': 0.37.2
|
'@mariozechner/pi-tui': 0.37.2
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@modelcontextprotocol/sdk'
|
- '@modelcontextprotocol/sdk'
|
||||||
@@ -3612,7 +3612,7 @@ snapshots:
|
|||||||
- ws
|
- ws
|
||||||
- zod
|
- zod
|
||||||
|
|
||||||
'@mariozechner/pi-ai@0.37.2(patch_hash=628fb051b6f4886984a846a5ee7aa0a571c3360d35b8d114e4684e5edcd100c5)(ws@8.19.0)(zod@4.3.5)':
|
'@mariozechner/pi-ai@0.37.2(patch_hash=b49275c3e2023970d8248ababef6df60e093e58a3ba3127c2ba4de1df387d06a)(ws@8.19.0)(zod@4.3.5)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@anthropic-ai/sdk': 0.71.2(zod@4.3.5)
|
'@anthropic-ai/sdk': 0.71.2(zod@4.3.5)
|
||||||
'@google/genai': 1.34.0
|
'@google/genai': 1.34.0
|
||||||
@@ -3636,7 +3636,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@crosscopy/clipboard': 0.2.8
|
'@crosscopy/clipboard': 0.2.8
|
||||||
'@mariozechner/pi-agent-core': 0.37.2(ws@8.19.0)(zod@4.3.5)
|
'@mariozechner/pi-agent-core': 0.37.2(ws@8.19.0)(zod@4.3.5)
|
||||||
'@mariozechner/pi-ai': 0.37.2(patch_hash=628fb051b6f4886984a846a5ee7aa0a571c3360d35b8d114e4684e5edcd100c5)(ws@8.19.0)(zod@4.3.5)
|
'@mariozechner/pi-ai': 0.37.2(patch_hash=b49275c3e2023970d8248ababef6df60e093e58a3ba3127c2ba4de1df387d06a)(ws@8.19.0)(zod@4.3.5)
|
||||||
'@mariozechner/pi-tui': 0.37.2
|
'@mariozechner/pi-tui': 0.37.2
|
||||||
chalk: 5.6.2
|
chalk: 5.6.2
|
||||||
cli-highlight: 2.1.11
|
cli-highlight: 2.1.11
|
||||||
|
|||||||
@@ -231,4 +231,252 @@ describe("google-shared convertMessages", () => {
|
|||||||
thoughtSignature: "sig",
|
thoughtSignature: "sig",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("merges consecutive user messages to satisfy Gemini role alternation", () => {
|
||||||
|
const model = makeModel("gemini-1.5-pro");
|
||||||
|
const context = {
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
role: "user",
|
||||||
|
content: "Hello",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: "user",
|
||||||
|
content: "How are you?",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
} as unknown as Context;
|
||||||
|
|
||||||
|
const contents = convertMessages(model, context);
|
||||||
|
// Should merge into a single user message
|
||||||
|
expect(contents).toHaveLength(1);
|
||||||
|
expect(contents[0].role).toBe("user");
|
||||||
|
expect(contents[0].parts).toHaveLength(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("merges consecutive user messages for non-Gemini Google models", () => {
|
||||||
|
const model = makeModel("claude-3-opus");
|
||||||
|
const context = {
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
role: "user",
|
||||||
|
content: "First",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: "user",
|
||||||
|
content: "Second",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
} as unknown as Context;
|
||||||
|
|
||||||
|
const contents = convertMessages(model, context);
|
||||||
|
expect(contents).toHaveLength(1);
|
||||||
|
expect(contents[0].role).toBe("user");
|
||||||
|
expect(contents[0].parts).toHaveLength(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("merges consecutive model messages to satisfy Gemini role alternation", () => {
|
||||||
|
const model = makeModel("gemini-1.5-pro");
|
||||||
|
const context = {
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
role: "user",
|
||||||
|
content: "Hello",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: "assistant",
|
||||||
|
content: [{ type: "text", text: "Hi there!" }],
|
||||||
|
api: "google-generative-ai",
|
||||||
|
provider: "google",
|
||||||
|
model: "gemini-1.5-pro",
|
||||||
|
usage: {
|
||||||
|
input: 0,
|
||||||
|
output: 0,
|
||||||
|
cacheRead: 0,
|
||||||
|
cacheWrite: 0,
|
||||||
|
totalTokens: 0,
|
||||||
|
cost: {
|
||||||
|
input: 0,
|
||||||
|
output: 0,
|
||||||
|
cacheRead: 0,
|
||||||
|
cacheWrite: 0,
|
||||||
|
total: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
stopReason: "stop",
|
||||||
|
timestamp: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: "assistant",
|
||||||
|
content: [{ type: "text", text: "How can I help?" }],
|
||||||
|
api: "google-generative-ai",
|
||||||
|
provider: "google",
|
||||||
|
model: "gemini-1.5-pro",
|
||||||
|
usage: {
|
||||||
|
input: 0,
|
||||||
|
output: 0,
|
||||||
|
cacheRead: 0,
|
||||||
|
cacheWrite: 0,
|
||||||
|
totalTokens: 0,
|
||||||
|
cost: {
|
||||||
|
input: 0,
|
||||||
|
output: 0,
|
||||||
|
cacheRead: 0,
|
||||||
|
cacheWrite: 0,
|
||||||
|
total: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
stopReason: "stop",
|
||||||
|
timestamp: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
} as unknown as Context;
|
||||||
|
|
||||||
|
const contents = convertMessages(model, context);
|
||||||
|
// Should have 1 user + 1 merged model message
|
||||||
|
expect(contents).toHaveLength(2);
|
||||||
|
expect(contents[0].role).toBe("user");
|
||||||
|
expect(contents[1].role).toBe("model");
|
||||||
|
expect(contents[1].parts).toHaveLength(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles user message after tool result without model response in between", () => {
|
||||||
|
const model = makeModel("gemini-1.5-pro");
|
||||||
|
const context = {
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
role: "user",
|
||||||
|
content: "Use a tool",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: "assistant",
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "toolCall",
|
||||||
|
id: "call_1",
|
||||||
|
name: "myTool",
|
||||||
|
arguments: { arg: "value" },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
api: "google-generative-ai",
|
||||||
|
provider: "google",
|
||||||
|
model: "gemini-1.5-pro",
|
||||||
|
usage: {
|
||||||
|
input: 0,
|
||||||
|
output: 0,
|
||||||
|
cacheRead: 0,
|
||||||
|
cacheWrite: 0,
|
||||||
|
totalTokens: 0,
|
||||||
|
cost: {
|
||||||
|
input: 0,
|
||||||
|
output: 0,
|
||||||
|
cacheRead: 0,
|
||||||
|
cacheWrite: 0,
|
||||||
|
total: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
stopReason: "stop",
|
||||||
|
timestamp: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: "toolResult",
|
||||||
|
toolCallId: "call_1",
|
||||||
|
toolName: "myTool",
|
||||||
|
content: [{ type: "text", text: "Tool result" }],
|
||||||
|
isError: false,
|
||||||
|
timestamp: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: "user",
|
||||||
|
content: "Now do something else",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
} as unknown as Context;
|
||||||
|
|
||||||
|
const contents = convertMessages(model, context);
|
||||||
|
// Tool result creates a user turn with functionResponse
|
||||||
|
// The next user message should be merged into it or there should be proper alternation
|
||||||
|
// Check that we don't have consecutive user messages
|
||||||
|
for (let i = 1; i < contents.length; i++) {
|
||||||
|
if (contents[i].role === "user" && contents[i - 1].role === "user") {
|
||||||
|
// If consecutive, they should have been merged
|
||||||
|
expect.fail("Consecutive user messages should be merged");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// The conversation should be valid for Gemini
|
||||||
|
expect(contents.length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("ensures function call comes after user turn, not after model turn", () => {
|
||||||
|
const model = makeModel("gemini-1.5-pro");
|
||||||
|
const context = {
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
role: "user",
|
||||||
|
content: "Hello",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: "assistant",
|
||||||
|
content: [{ type: "text", text: "Hi!" }],
|
||||||
|
api: "google-generative-ai",
|
||||||
|
provider: "google",
|
||||||
|
model: "gemini-1.5-pro",
|
||||||
|
usage: {
|
||||||
|
input: 0,
|
||||||
|
output: 0,
|
||||||
|
cacheRead: 0,
|
||||||
|
cacheWrite: 0,
|
||||||
|
totalTokens: 0,
|
||||||
|
cost: {
|
||||||
|
input: 0,
|
||||||
|
output: 0,
|
||||||
|
cacheRead: 0,
|
||||||
|
cacheWrite: 0,
|
||||||
|
total: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
stopReason: "stop",
|
||||||
|
timestamp: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: "assistant",
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "toolCall",
|
||||||
|
id: "call_1",
|
||||||
|
name: "myTool",
|
||||||
|
arguments: {},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
api: "google-generative-ai",
|
||||||
|
provider: "google",
|
||||||
|
model: "gemini-1.5-pro",
|
||||||
|
usage: {
|
||||||
|
input: 0,
|
||||||
|
output: 0,
|
||||||
|
cacheRead: 0,
|
||||||
|
cacheWrite: 0,
|
||||||
|
totalTokens: 0,
|
||||||
|
cost: {
|
||||||
|
input: 0,
|
||||||
|
output: 0,
|
||||||
|
cacheRead: 0,
|
||||||
|
cacheWrite: 0,
|
||||||
|
total: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
stopReason: "stop",
|
||||||
|
timestamp: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
} as unknown as Context;
|
||||||
|
|
||||||
|
const contents = convertMessages(model, context);
|
||||||
|
// Consecutive model messages should be merged so function call is in same turn as text
|
||||||
|
expect(contents).toHaveLength(2);
|
||||||
|
expect(contents[0].role).toBe("user");
|
||||||
|
expect(contents[1].role).toBe("model");
|
||||||
|
// The model message should have both text and function call
|
||||||
|
expect(contents[1].parts?.length).toBe(2);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user