@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.
Files changed (68) hide show
  1. package/dist/cjs/agents/AgentContext.cjs +1 -1
  2. package/dist/cjs/common/enum.cjs +2 -0
  3. package/dist/cjs/common/enum.cjs.map +1 -1
  4. package/dist/cjs/graphs/Graph.cjs +14 -1
  5. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  6. package/dist/cjs/graphs/MultiAgentGraph.cjs +1 -1
  7. package/dist/cjs/langfuseToolOutputTracing.cjs +4 -0
  8. package/dist/cjs/langfuseToolOutputTracing.cjs.map +1 -1
  9. package/dist/cjs/llm/google/index.cjs +2 -0
  10. package/dist/cjs/llm/google/index.cjs.map +1 -1
  11. package/dist/cjs/llm/google/utils/common.cjs +28 -0
  12. package/dist/cjs/llm/google/utils/common.cjs.map +1 -1
  13. package/dist/cjs/llm/openai/index.cjs +1 -1
  14. package/dist/cjs/main.cjs +1 -0
  15. package/dist/cjs/messages/format.cjs +136 -4
  16. package/dist/cjs/messages/format.cjs.map +1 -1
  17. package/dist/cjs/prompts/activityLabel.cjs +101 -0
  18. package/dist/cjs/prompts/activityLabel.cjs.map +1 -0
  19. package/dist/cjs/run.cjs +162 -1
  20. package/dist/cjs/run.cjs.map +1 -1
  21. package/dist/cjs/tools/subagent/SubagentExecutor.cjs +1 -1
  22. package/dist/esm/agents/AgentContext.mjs +1 -1
  23. package/dist/esm/common/enum.mjs +2 -0
  24. package/dist/esm/common/enum.mjs.map +1 -1
  25. package/dist/esm/graphs/Graph.mjs +15 -2
  26. package/dist/esm/graphs/Graph.mjs.map +1 -1
  27. package/dist/esm/graphs/MultiAgentGraph.mjs +1 -1
  28. package/dist/esm/langfuseToolOutputTracing.mjs +4 -1
  29. package/dist/esm/langfuseToolOutputTracing.mjs.map +1 -1
  30. package/dist/esm/llm/google/index.mjs +3 -1
  31. package/dist/esm/llm/google/index.mjs.map +1 -1
  32. package/dist/esm/llm/google/utils/common.mjs +28 -1
  33. package/dist/esm/llm/google/utils/common.mjs.map +1 -1
  34. package/dist/esm/llm/openai/index.mjs +1 -1
  35. package/dist/esm/main.mjs +2 -2
  36. package/dist/esm/messages/format.mjs +136 -5
  37. package/dist/esm/messages/format.mjs.map +1 -1
  38. package/dist/esm/prompts/activityLabel.mjs +100 -0
  39. package/dist/esm/prompts/activityLabel.mjs.map +1 -0
  40. package/dist/esm/run.mjs +163 -2
  41. package/dist/esm/run.mjs.map +1 -1
  42. package/dist/esm/tools/subagent/SubagentExecutor.mjs +1 -1
  43. package/dist/types/common/enum.d.ts +3 -1
  44. package/dist/types/langfuseToolOutputTracing.d.ts +4 -0
  45. package/dist/types/llm/google/utils/common.d.ts +9 -0
  46. package/dist/types/messages/format.d.ts +22 -0
  47. package/dist/types/prompts/activityLabel.d.ts +31 -0
  48. package/dist/types/run.d.ts +14 -0
  49. package/dist/types/types/activityLabel.d.ts +53 -0
  50. package/dist/types/types/index.d.ts +1 -0
  51. package/dist/types/types/stream.d.ts +2 -0
  52. package/package.json +1 -1
  53. package/src/common/enum.ts +2 -0
  54. package/src/graphs/Graph.ts +20 -0
  55. package/src/langfuseToolOutputTracing.ts +4 -1
  56. package/src/llm/google/index.ts +3 -0
  57. package/src/llm/google/utils/common.test.ts +57 -2
  58. package/src/llm/google/utils/common.ts +44 -0
  59. package/src/messages/foldToollessToolBlocks.test.ts +438 -0
  60. package/src/messages/format.ts +233 -5
  61. package/src/prompts/activityLabel.ts +177 -0
  62. package/src/run.ts +298 -2
  63. package/src/specs/activity-label-prompt.test.ts +128 -0
  64. package/src/specs/activity-label-trace-seed.test.ts +47 -0
  65. package/src/specs/bedrock-toolless.live.test.ts +123 -0
  66. package/src/types/activityLabel.ts +55 -0
  67. package/src/types/index.ts +1 -0
  68. 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;
@@ -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
+ };
@@ -7,3 +7,4 @@ export * from './skill';
7
7
  export * from './stream';
8
8
  export * from './tools';
9
9
  export * from './summarize';
10
+ export * from './activityLabel';
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@librechat/agents",
3
- "version": "3.2.67",
3
+ "version": "3.3.0",
4
4
  "main": "./dist/cjs/main.cjs",
5
5
  "module": "./dist/esm/main.mjs",
6
6
  "types": "./dist/types/index.d.ts",
@@ -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 {
@@ -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
- function shouldRedactTool(
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 {
@@ -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 { convertResponseContentToChatGenerationChunk } from './common';
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: {