@librechat/agents 3.2.67 → 3.3.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/dist/cjs/agents/AgentContext.cjs +1 -1
- package/dist/cjs/common/enum.cjs +2 -0
- package/dist/cjs/common/enum.cjs.map +1 -1
- package/dist/cjs/graphs/Graph.cjs +14 -1
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/graphs/MultiAgentGraph.cjs +1 -1
- package/dist/cjs/langfuseToolOutputTracing.cjs +4 -0
- package/dist/cjs/langfuseToolOutputTracing.cjs.map +1 -1
- package/dist/cjs/llm/google/index.cjs +2 -0
- package/dist/cjs/llm/google/index.cjs.map +1 -1
- package/dist/cjs/llm/google/utils/common.cjs +28 -0
- package/dist/cjs/llm/google/utils/common.cjs.map +1 -1
- package/dist/cjs/llm/openai/index.cjs +1 -1
- package/dist/cjs/main.cjs +1 -0
- package/dist/cjs/messages/format.cjs +136 -4
- package/dist/cjs/messages/format.cjs.map +1 -1
- package/dist/cjs/prompts/activityLabel.cjs +101 -0
- package/dist/cjs/prompts/activityLabel.cjs.map +1 -0
- package/dist/cjs/run.cjs +162 -1
- package/dist/cjs/run.cjs.map +1 -1
- package/dist/cjs/tools/subagent/SubagentExecutor.cjs +1 -1
- package/dist/esm/agents/AgentContext.mjs +1 -1
- package/dist/esm/common/enum.mjs +2 -0
- package/dist/esm/common/enum.mjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +15 -2
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/graphs/MultiAgentGraph.mjs +1 -1
- package/dist/esm/langfuseToolOutputTracing.mjs +4 -1
- package/dist/esm/langfuseToolOutputTracing.mjs.map +1 -1
- package/dist/esm/llm/google/index.mjs +3 -1
- package/dist/esm/llm/google/index.mjs.map +1 -1
- package/dist/esm/llm/google/utils/common.mjs +28 -1
- package/dist/esm/llm/google/utils/common.mjs.map +1 -1
- package/dist/esm/llm/openai/index.mjs +1 -1
- package/dist/esm/main.mjs +2 -2
- package/dist/esm/messages/format.mjs +136 -5
- package/dist/esm/messages/format.mjs.map +1 -1
- package/dist/esm/prompts/activityLabel.mjs +100 -0
- package/dist/esm/prompts/activityLabel.mjs.map +1 -0
- package/dist/esm/run.mjs +163 -2
- package/dist/esm/run.mjs.map +1 -1
- package/dist/esm/tools/subagent/SubagentExecutor.mjs +1 -1
- package/dist/types/common/enum.d.ts +3 -1
- package/dist/types/langfuseToolOutputTracing.d.ts +4 -0
- package/dist/types/llm/google/utils/common.d.ts +9 -0
- package/dist/types/messages/format.d.ts +22 -0
- package/dist/types/prompts/activityLabel.d.ts +31 -0
- package/dist/types/run.d.ts +14 -0
- package/dist/types/types/activityLabel.d.ts +53 -0
- package/dist/types/types/index.d.ts +1 -0
- package/dist/types/types/stream.d.ts +2 -0
- package/package.json +1 -1
- package/src/common/enum.ts +2 -0
- package/src/graphs/Graph.ts +20 -0
- package/src/langfuseToolOutputTracing.ts +4 -1
- package/src/llm/google/index.ts +3 -0
- package/src/llm/google/utils/common.test.ts +57 -2
- package/src/llm/google/utils/common.ts +44 -0
- package/src/messages/foldToollessToolBlocks.test.ts +438 -0
- package/src/messages/format.ts +233 -5
- package/src/prompts/activityLabel.ts +177 -0
- package/src/run.ts +298 -2
- package/src/specs/activity-label-prompt.test.ts +128 -0
- package/src/specs/activity-label-trace-seed.test.ts +47 -0
- package/src/specs/bedrock-toolless.live.test.ts +123 -0
- package/src/types/activityLabel.ts +55 -0
- package/src/types/index.ts +1 -0
- package/src/types/stream.ts +2 -0
|
@@ -21,6 +21,15 @@ export declare function getMessageAuthor(message: BaseMessage): string;
|
|
|
21
21
|
export declare function convertAuthorToRole(author: string): (typeof POSSIBLE_ROLES)[number];
|
|
22
22
|
export declare function convertMessageContentToParts(message: BaseMessage, isMultimodalModel: boolean, previousMessages: BaseMessage[], model?: string): Part[];
|
|
23
23
|
export declare function convertBaseMessagesToContent(messages: BaseMessage[], isMultimodalModel: boolean, convertSystemMessageToHumanContent?: boolean, model?: string): Content[] | undefined;
|
|
24
|
+
export declare function rejectsModelTurnPrefill(model?: string): boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Drops trailing `model`-role turns for models that reject prefill (see
|
|
27
|
+
* {@link rejectsModelTurnPrefill}). Such a turn is only produced by prefill
|
|
28
|
+
* flows (e.g. editing an assistant reply and resubmitting); these models return
|
|
29
|
+
* HTTP 400 for it, so we drop it and let the model generate fresh from the
|
|
30
|
+
* preceding user turn. No-op for every other model, preserving working prefill.
|
|
31
|
+
*/
|
|
32
|
+
export declare function dropUnsupportedModelTurnPrefill(contents: Content[] | undefined, model?: string): Content[] | undefined;
|
|
24
33
|
export declare function convertResponseContentToChatGenerationChunk(response: EnhancedGenerateContentResponse, extra: {
|
|
25
34
|
usageMetadata?: UsageMetadata | undefined;
|
|
26
35
|
index: number;
|
|
@@ -178,4 +178,26 @@ export declare function shiftIndexTokenCountMap(indexTokenCountMap: Record<numbe
|
|
|
178
178
|
* @returns The messages array with tool sequences converted to buffer strings if necessary
|
|
179
179
|
*/
|
|
180
180
|
export declare function ensureThinkingBlockInMessages(messages: BaseMessage[], _provider: Providers, config?: RunnableConfig, runStartIndex?: number): BaseMessage[];
|
|
181
|
+
/**
|
|
182
|
+
* Folds tool_use / tool_result content into plain text for an agent that binds
|
|
183
|
+
* no tools.
|
|
184
|
+
*
|
|
185
|
+
* In a multi-agent graph, a tool-less destination still inherits the prior
|
|
186
|
+
* agent's conversation history, which can contain toolUse/toolResult blocks.
|
|
187
|
+
* Because it binds no tools, the model is invoked with no tool schema — and
|
|
188
|
+
* Bedrock's Converse API rejects any request that carries toolUse/toolResult
|
|
189
|
+
* blocks without a top-level toolConfig ("The toolConfig field must be defined
|
|
190
|
+
* when using toolUse and toolResult content blocks"). Adding a dummy toolConfig
|
|
191
|
+
* is not an option: AWS requires at least one tool, and it would expose a
|
|
192
|
+
* capability the destination was intentionally denied.
|
|
193
|
+
*
|
|
194
|
+
* Each tool-call turn plus its trailing tool results (ToolMessages or
|
|
195
|
+
* `tool_result` content blocks) is collapsed into a single `[Previous tool
|
|
196
|
+
* interaction]` HumanMessage that preserves the tool name, arguments and result
|
|
197
|
+
* as text (image blocks are kept as-is). Runs in a single pass: non-tool
|
|
198
|
+
* messages pass through, `result` is allocated lazily on the first fold, and the
|
|
199
|
+
* original array is returned unchanged when it holds no tool content (the common
|
|
200
|
+
* fresh-tool-less-agent case).
|
|
201
|
+
*/
|
|
202
|
+
export declare function foldToolBlocksForToollessAgent(messages: BaseMessage[], config?: RunnableConfig): BaseMessage[];
|
|
181
203
|
export {};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { ResolvedLangfuseToolOutputTracingConfig } from '@/langfuseRuntimeContext';
|
|
2
|
+
import type { ActivityLabelToolEntry } from '@/types/activityLabel';
|
|
3
|
+
/**
|
|
4
|
+
* Default system prompt for fast-model activity labeling.
|
|
5
|
+
*
|
|
6
|
+
* Style synthesized from Claude Code's tool-use summary prompt (git-subject
|
|
7
|
+
* register, past tense, distinctive nouns) and claude.ai's observed group
|
|
8
|
+
* headers (5–9 words describing a mixed reasoning + tool block, e.g.
|
|
9
|
+
* "Synthesized version data and curated comparative framework").
|
|
10
|
+
*/
|
|
11
|
+
export declare const ACTIVITY_LABEL_PROMPT = "Write a short label describing what this block of agent activity accomplished. It appears as the header of a collapsed activity group in a chat UI.\n\nRules:\n- 5 to 9 words, past-tense verb first\n- Name the most distinctive subject (file, API, topic); drop articles and filler\n- Describe outcomes, not mechanics; if something failed, say so plainly\n- Output only the label \u2014 no quotes, no punctuation at the end, no preamble\n\nExamples:\n- Searched Node.js release notes and changelogs\n- Compared runtime versions across official sources\n- Fixed failing auth middleware tests\n- Read project config and dependency manifests\n- Attempted database migration, hit permission errors";
|
|
12
|
+
/** Truncates a serialized value for the label prompt. */
|
|
13
|
+
export declare function truncateForLabel(value: string, maxLength: number): string;
|
|
14
|
+
export type BuildActivityLabelPromptParams = {
|
|
15
|
+
entries: ActivityLabelToolEntry[];
|
|
16
|
+
charLimit: number;
|
|
17
|
+
thinkingExcerpts?: string[];
|
|
18
|
+
lastAssistantText?: string;
|
|
19
|
+
/**
|
|
20
|
+
* Resolved tool-output tracing policy. The label prompt becomes Langfuse
|
|
21
|
+
* generation input, so outputs/errors excluded from tracing (global
|
|
22
|
+
* disable or `redactedToolNames`) must never appear in it — the same
|
|
23
|
+
* redaction the span processor applies to structured tool observations.
|
|
24
|
+
*/
|
|
25
|
+
redaction?: ResolvedLangfuseToolOutputTracingConfig;
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Builds the user prompt for a fast-model activity label. Pure — exported
|
|
29
|
+
* for direct testing of redaction and truncation behavior.
|
|
30
|
+
*/
|
|
31
|
+
export declare function buildActivityLabelPrompt({ entries, charLimit, thinkingExcerpts, lastAssistantText, redaction, }: BuildActivityLabelPromptParams): string;
|
package/dist/types/run.d.ts
CHANGED
|
@@ -39,6 +39,8 @@ export declare class Run<_T extends t.BaseGraphState> {
|
|
|
39
39
|
* lets callers assert the type they expect.
|
|
40
40
|
*/
|
|
41
41
|
private _interrupt;
|
|
42
|
+
/** Per-run sequence for batch-unique activity-label trace-seed fallbacks. */
|
|
43
|
+
private activityLabelSeq;
|
|
42
44
|
private _haltedReason;
|
|
43
45
|
private constructor();
|
|
44
46
|
private createLegacyGraph;
|
|
@@ -207,4 +209,16 @@ export declare class Run<_T extends t.BaseGraphState> {
|
|
|
207
209
|
language?: string;
|
|
208
210
|
title?: string;
|
|
209
211
|
}>;
|
|
212
|
+
/**
|
|
213
|
+
* Generates a short activity label for a completed tool/reasoning block
|
|
214
|
+
* using a fast model. Mirrors `generateTitle`'s Langfuse wiring so the
|
|
215
|
+
* call is traced under the conversation's session (sessionId from
|
|
216
|
+
* `chainOptions.configurable.thread_id`) with its own tags — never as an
|
|
217
|
+
* orphan trace. The payload contains no human messages by design: intent
|
|
218
|
+
* comes from `lastAssistantText`, content from reasoning excerpts and
|
|
219
|
+
* tool entries.
|
|
220
|
+
*/
|
|
221
|
+
generateActivityLabel({ provider, clientOptions, entries, thinkingExcerpts, lastAssistantText, prompt, charLimit, chainOptions, traceSeed, agentId, }: t.RunActivityLabelOptions): Promise<{
|
|
222
|
+
label?: string;
|
|
223
|
+
}>;
|
|
210
224
|
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { RunnableConfig } from '@langchain/core/runnables';
|
|
2
|
+
import type { ClientOptions } from '@/types/llm';
|
|
3
|
+
import type { Providers } from '@/common';
|
|
4
|
+
/** One tool call's contribution to the label payload (host-assembled). */
|
|
5
|
+
export type ActivityLabelToolEntry = {
|
|
6
|
+
toolName: string;
|
|
7
|
+
toolInput: unknown;
|
|
8
|
+
toolOutput?: unknown;
|
|
9
|
+
error?: string;
|
|
10
|
+
status: 'success' | 'error';
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Options for `Run.generateActivityLabel`. The payload deliberately contains
|
|
14
|
+
* NO human messages: intent context comes from the assistant's own last text
|
|
15
|
+
* (Claude Code's pattern) and the block's reasoning excerpts (claude.ai's
|
|
16
|
+
* pattern) — user text stays out of this low-scrutiny pathway entirely.
|
|
17
|
+
*
|
|
18
|
+
* This SDK defines NO activity-label graph event and never dispatches one.
|
|
19
|
+
* Label lifecycle streaming is entirely host-owned: a host claims its own
|
|
20
|
+
* content slots and emits on its own transport, with a payload shape only
|
|
21
|
+
* it defines. The SDK surface here is exactly this method plus the
|
|
22
|
+
* `activity_label` content type's formatter exclusions.
|
|
23
|
+
*/
|
|
24
|
+
export type RunActivityLabelOptions = {
|
|
25
|
+
provider: Providers;
|
|
26
|
+
clientOptions?: ClientOptions;
|
|
27
|
+
/**
|
|
28
|
+
* Agent that executed the labeled batch. Selects that agent's Langfuse
|
|
29
|
+
* overlay (trace metadata AND tool-output redaction policy) instead of
|
|
30
|
+
* the graph default — a stricter per-agent policy must not be bypassed
|
|
31
|
+
* by labeling work the default agent never performed.
|
|
32
|
+
*/
|
|
33
|
+
agentId?: string;
|
|
34
|
+
entries: ActivityLabelToolEntry[];
|
|
35
|
+
/** Truncated reasoning excerpts from the block being labeled. */
|
|
36
|
+
thinkingExcerpts?: string[];
|
|
37
|
+
/** Assistant's last text before the block (~200 chars), as intent context. */
|
|
38
|
+
lastAssistantText?: string;
|
|
39
|
+
/** Override for the default label system prompt. */
|
|
40
|
+
prompt?: string;
|
|
41
|
+
/** Per-entry serialization cap for the prompt. Default 600. */
|
|
42
|
+
charLimit?: number;
|
|
43
|
+
/** LangChain runnable config carrier (signal, callbacks, thread/user ids). */
|
|
44
|
+
chainOptions?: Partial<RunnableConfig> & {
|
|
45
|
+
configurable?: Record<string, unknown>;
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* Seed for deterministic Langfuse trace ids (e.g. `${runId}-${slotIndex}`)
|
|
49
|
+
* so each batch's label gets a distinct, reproducible trace. When omitted,
|
|
50
|
+
* a per-run sequence keeps batches from collapsing into one trace.
|
|
51
|
+
*/
|
|
52
|
+
traceSeed?: string;
|
|
53
|
+
};
|
|
@@ -138,6 +138,8 @@ export interface ExtendedMessageContent {
|
|
|
138
138
|
type?: string;
|
|
139
139
|
text?: string;
|
|
140
140
|
input?: string;
|
|
141
|
+
/** Tool-call arguments on a v1 standard-content `tool_call` block. */
|
|
142
|
+
args?: ToolCallPart['args'];
|
|
141
143
|
index?: string | number;
|
|
142
144
|
id?: string;
|
|
143
145
|
name?: string;
|
package/package.json
CHANGED
package/src/common/enum.ts
CHANGED
|
@@ -142,6 +142,8 @@ export enum ContentTypes {
|
|
|
142
142
|
REASONING_CONTENT = 'reasoning_content',
|
|
143
143
|
/** Mid-run user steer persisted inline in an assistant message; replayed as a user turn */
|
|
144
144
|
STEER = 'steer',
|
|
145
|
+
/** Fast-model activity label for a tool/reasoning block; UI-only, never model input */
|
|
146
|
+
ACTIVITY_LABEL = 'activity_label',
|
|
145
147
|
}
|
|
146
148
|
|
|
147
149
|
export enum ToolCallTypes {
|
package/src/graphs/Graph.ts
CHANGED
|
@@ -16,6 +16,7 @@ import type * as t from '@/types';
|
|
|
16
16
|
import {
|
|
17
17
|
formatAnthropicArtifactContent,
|
|
18
18
|
ensureThinkingBlockInMessages,
|
|
19
|
+
foldToolBlocksForToollessAgent,
|
|
19
20
|
convertMessagesToContent,
|
|
20
21
|
sanitizeOrphanToolBlocks,
|
|
21
22
|
extractToolDiscoveries,
|
|
@@ -1864,6 +1865,25 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
1864
1865
|
);
|
|
1865
1866
|
}
|
|
1866
1867
|
|
|
1868
|
+
/**
|
|
1869
|
+
* A destination that binds no tools is invoked without a tool schema, but
|
|
1870
|
+
* in a multi-agent graph it can still inherit a prior agent's toolUse/
|
|
1871
|
+
* toolResult history. Bedrock's Converse API (and other tool-schema-strict
|
|
1872
|
+
* providers) reject such a request when no top-level toolConfig is sent.
|
|
1873
|
+
* Fold that historical tool content into plain text so the tool-less agent
|
|
1874
|
+
* receives valid, context-preserving messages. Handoff tools count as
|
|
1875
|
+
* bound tools, so a tool-less router mid-handoff is not affected.
|
|
1876
|
+
*/
|
|
1877
|
+
if (toolsForBinding == null || toolsForBinding.length === 0) {
|
|
1878
|
+
finalMessages = foldToolBlocksForToollessAgent(finalMessages, config);
|
|
1879
|
+
// The fold emits structured (array) content; re-flatten for agents that
|
|
1880
|
+
// opted into string-only messages (`useLegacyContent`, run earlier at
|
|
1881
|
+
// the top of this block) so the folded turn isn't the lone exception.
|
|
1882
|
+
if (agentContext.useLegacyContent) {
|
|
1883
|
+
finalMessages = formatContentStrings(finalMessages);
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1886
|
+
|
|
1867
1887
|
// Determine the prompt-cache strategy up front. Two distinct facts:
|
|
1868
1888
|
//
|
|
1869
1889
|
// `providerPromptCacheEnabled` — prompt caching is on for this provider
|
|
@@ -84,7 +84,10 @@ function toolNameMatches(
|
|
|
84
84
|
return config.redactedToolNames.has(normalizedToolName);
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
-
|
|
87
|
+
/** Whether a tool's outputs are excluded from tracing (global disable or
|
|
88
|
+
* `redactedToolNames` match). Exported for the activity-label prompt
|
|
89
|
+
* builder, whose prompt becomes Langfuse generation input. */
|
|
90
|
+
export function shouldRedactTool(
|
|
88
91
|
toolName: string | undefined,
|
|
89
92
|
config: ResolvedLangfuseToolOutputTracingConfig
|
|
90
93
|
): boolean {
|
package/src/llm/google/index.ts
CHANGED
|
@@ -19,6 +19,7 @@ import type { GoogleClientOptions, GoogleThinkingConfig } from '@/types';
|
|
|
19
19
|
import {
|
|
20
20
|
convertResponseContentToChatGenerationChunk,
|
|
21
21
|
convertBaseMessagesToContent,
|
|
22
|
+
dropUnsupportedModelTurnPrefill,
|
|
22
23
|
mapGenerateContentResultToChatResult,
|
|
23
24
|
} from './utils/common';
|
|
24
25
|
|
|
@@ -254,6 +255,7 @@ export class CustomChatGoogleGenerativeAI extends ChatGoogleGenerativeAI {
|
|
|
254
255
|
this.client.systemInstruction = systemInstruction;
|
|
255
256
|
actualPrompt = prompt.slice(1);
|
|
256
257
|
}
|
|
258
|
+
actualPrompt = dropUnsupportedModelTurnPrefill(actualPrompt, this.model);
|
|
257
259
|
const parameters = this.invocationParams(options);
|
|
258
260
|
const request = {
|
|
259
261
|
...parameters,
|
|
@@ -308,6 +310,7 @@ export class CustomChatGoogleGenerativeAI extends ChatGoogleGenerativeAI {
|
|
|
308
310
|
this.client.systemInstruction = systemInstruction;
|
|
309
311
|
actualPrompt = prompt.slice(1);
|
|
310
312
|
}
|
|
313
|
+
actualPrompt = dropUnsupportedModelTurnPrefill(actualPrompt, this.model);
|
|
311
314
|
const parameters = this.invocationParams(options);
|
|
312
315
|
const request = {
|
|
313
316
|
...parameters,
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import { expect, test, describe } from '@jest/globals';
|
|
2
2
|
import { AIMessageChunk } from '@langchain/core/messages';
|
|
3
|
-
import type { EnhancedGenerateContentResponse } from '@google/generative-ai';
|
|
3
|
+
import type { Content, EnhancedGenerateContentResponse } from '@google/generative-ai';
|
|
4
4
|
import {
|
|
5
5
|
STREAMED_TOOL_CALL_SEAL_METADATA_KEY,
|
|
6
6
|
STREAMED_TOOL_CALL_ADAPTER_METADATA_KEY,
|
|
7
7
|
GOOGLE_STREAMED_TOOL_CALL_ADAPTER,
|
|
8
8
|
} from '@/tools/streamedToolCallSeals';
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
convertResponseContentToChatGenerationChunk,
|
|
11
|
+
dropUnsupportedModelTurnPrefill,
|
|
12
|
+
rejectsModelTurnPrefill,
|
|
13
|
+
} from './common';
|
|
10
14
|
|
|
11
15
|
function buildResponse(
|
|
12
16
|
parts: Array<Record<string, unknown>>
|
|
@@ -62,3 +66,54 @@ describe('convertResponseContentToChatGenerationChunk seal metadata', () => {
|
|
|
62
66
|
expect(metadata[STREAMED_TOOL_CALL_SEAL_METADATA_KEY]).toBeUndefined();
|
|
63
67
|
});
|
|
64
68
|
});
|
|
69
|
+
|
|
70
|
+
describe('rejectsModelTurnPrefill', () => {
|
|
71
|
+
test('is true for models that reject a trailing model turn', () => {
|
|
72
|
+
expect(rejectsModelTurnPrefill('gemini-3.6-flash')).toBe(true);
|
|
73
|
+
expect(rejectsModelTurnPrefill('gemini-3.5-flash-lite')).toBe(true);
|
|
74
|
+
expect(rejectsModelTurnPrefill('models/gemini-3.6-flash')).toBe(true);
|
|
75
|
+
expect(rejectsModelTurnPrefill('google/gemini-3.5-flash-lite-latest')).toBe(true);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('is false for models that still accept prefill and for empty input', () => {
|
|
79
|
+
expect(rejectsModelTurnPrefill('gemini-3.5-flash')).toBe(false);
|
|
80
|
+
expect(rejectsModelTurnPrefill('gemini-2.5-flash')).toBe(false);
|
|
81
|
+
expect(rejectsModelTurnPrefill('gemini-3-pro-preview')).toBe(false);
|
|
82
|
+
expect(rejectsModelTurnPrefill(undefined)).toBe(false);
|
|
83
|
+
expect(rejectsModelTurnPrefill('')).toBe(false);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe('dropUnsupportedModelTurnPrefill', () => {
|
|
88
|
+
const userTurn: Content = { role: 'user', parts: [{ text: 'Hi' }] };
|
|
89
|
+
const modelTurn: Content = { role: 'model', parts: [{ text: 'Hello, I am' }] };
|
|
90
|
+
|
|
91
|
+
test('drops a trailing model turn for no-prefill models', () => {
|
|
92
|
+
const contents: Content[] = [userTurn, modelTurn];
|
|
93
|
+
const result = dropUnsupportedModelTurnPrefill(contents, 'gemini-3.6-flash');
|
|
94
|
+
expect(result).toEqual([userTurn]);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test('drops multiple consecutive trailing model turns but keeps one turn', () => {
|
|
98
|
+
const contents: Content[] = [userTurn, modelTurn, modelTurn];
|
|
99
|
+
const result = dropUnsupportedModelTurnPrefill(contents, 'gemini-3.5-flash-lite');
|
|
100
|
+
expect(result).toEqual([userTurn]);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('leaves a trailing model turn for models that accept prefill', () => {
|
|
104
|
+
const contents: Content[] = [userTurn, modelTurn];
|
|
105
|
+
const result = dropUnsupportedModelTurnPrefill(contents, 'gemini-3.5-flash');
|
|
106
|
+
expect(result).toBe(contents);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test('is a no-op when the request already ends with a user turn', () => {
|
|
110
|
+
const contents: Content[] = [modelTurn, userTurn];
|
|
111
|
+
const result = dropUnsupportedModelTurnPrefill(contents, 'gemini-3.6-flash');
|
|
112
|
+
expect(result).toBe(contents);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test('is a no-op for empty or undefined contents', () => {
|
|
116
|
+
expect(dropUnsupportedModelTurnPrefill([], 'gemini-3.6-flash')).toEqual([]);
|
|
117
|
+
expect(dropUnsupportedModelTurnPrefill(undefined, 'gemini-3.6-flash')).toBeUndefined();
|
|
118
|
+
});
|
|
119
|
+
});
|
|
@@ -631,6 +631,50 @@ export function convertBaseMessagesToContent(
|
|
|
631
631
|
).content;
|
|
632
632
|
}
|
|
633
633
|
|
|
634
|
+
/**
|
|
635
|
+
* Gemini models that reject a request whose `contents` end with a `model`-role
|
|
636
|
+
* turn (a "prefill"). Google enforces this on newer generations (Gemini 3.6
|
|
637
|
+
* Flash, Gemini 3.5 Flash-Lite) while older/sibling models still accept a
|
|
638
|
+
* trailing model turn, so the rule is model-scoped rather than version-wide.
|
|
639
|
+
* Extend this list as Google applies the restriction to further models.
|
|
640
|
+
* @see https://ai.google.dev/gemini-api/docs/latest-model#api-changes-and-parameter-updates
|
|
641
|
+
*/
|
|
642
|
+
const NO_PREFILL_GEMINI_MODELS = [
|
|
643
|
+
'gemini-3.6-flash',
|
|
644
|
+
'gemini-3.5-flash-lite',
|
|
645
|
+
] as const;
|
|
646
|
+
|
|
647
|
+
export function rejectsModelTurnPrefill(model?: string): boolean {
|
|
648
|
+
if (model == null || model === '') {
|
|
649
|
+
return false;
|
|
650
|
+
}
|
|
651
|
+
const modelId = model.toLowerCase().split('/').pop() ?? '';
|
|
652
|
+
return NO_PREFILL_GEMINI_MODELS.some(
|
|
653
|
+
(id) => modelId === id || modelId.startsWith(`${id}-`)
|
|
654
|
+
);
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
/**
|
|
658
|
+
* Drops trailing `model`-role turns for models that reject prefill (see
|
|
659
|
+
* {@link rejectsModelTurnPrefill}). Such a turn is only produced by prefill
|
|
660
|
+
* flows (e.g. editing an assistant reply and resubmitting); these models return
|
|
661
|
+
* HTTP 400 for it, so we drop it and let the model generate fresh from the
|
|
662
|
+
* preceding user turn. No-op for every other model, preserving working prefill.
|
|
663
|
+
*/
|
|
664
|
+
export function dropUnsupportedModelTurnPrefill(
|
|
665
|
+
contents: Content[] | undefined,
|
|
666
|
+
model?: string
|
|
667
|
+
): Content[] | undefined {
|
|
668
|
+
if (contents == null || contents.length === 0 || !rejectsModelTurnPrefill(model)) {
|
|
669
|
+
return contents;
|
|
670
|
+
}
|
|
671
|
+
let end = contents.length;
|
|
672
|
+
while (end > 1 && contents[end - 1]?.role === 'model') {
|
|
673
|
+
end -= 1;
|
|
674
|
+
}
|
|
675
|
+
return end === contents.length ? contents : contents.slice(0, end);
|
|
676
|
+
}
|
|
677
|
+
|
|
634
678
|
export function convertResponseContentToChatGenerationChunk(
|
|
635
679
|
response: EnhancedGenerateContentResponse,
|
|
636
680
|
extra: {
|