Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 12 additions & 12 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@librechat/agents",
"version": "3.3.12",
"version": "3.3.13",
"main": "./dist/cjs/main.cjs",
"module": "./dist/esm/main.mjs",
"types": "./dist/types/index.d.ts",
Expand Down Expand Up @@ -248,7 +248,7 @@
"nanoid": "^3.3.7",
"okapibm25": "^1.4.1",
"openai": "^6.46.0",
"reo-census": "^1.2.9",
"reo-census": "^1.2.10",
"socks-proxy-agent": "^8.0.5",
"uuid": "^11.1.1"
},
Expand Down
11 changes: 11 additions & 0 deletions src/graphs/Graph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -998,6 +998,7 @@ export abstract class Graph<

export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
overrideModel?: t.ChatModel;
private subagentModelOverride?: t.ChatModel;
/** Optional compile options passed into workflow.compile() */
compileOptions?: t.CompileOptions | undefined;
/** Whether the workflow was actually compiled with a checkpointer. */
Expand Down Expand Up @@ -1320,6 +1321,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
super.clearHeavyState();
this.messages = [];
this.overrideModel = undefined;
this.subagentModelOverride = undefined;
/** Stream-limit accounting (argument tallies, event counts, charge
* credits) deliberately SURVIVES cleanup: this runs in `processStream`'s
* finally, which an ordinary parallel-branch failure reaches while
Expand Down Expand Up @@ -1944,6 +1946,11 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
});
}

/** Explicitly overrides the model used by isolated descendant subagent graphs. */
setSubagentModelOverride(model: t.ChatModel): void {
this.subagentModelOverride = model;
}

getUsageMetadata(
finalMessage?: BaseMessage
): Partial<UsageMetadata> | undefined {
Expand Down Expand Up @@ -4057,6 +4064,10 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
maxDepth: effectiveSubagentDepth,
createChildGraph: (input): StandardGraph => {
const childGraph = new StandardGraph(input);
if (this.subagentModelOverride != null) {
childGraph.overrideModel = this.subagentModelOverride;
childGraph.setSubagentModelOverride(this.subagentModelOverride);
}
const toolHandlerRegistry = createToolHandlerRegistry(
getParentHandlerRegistry()
);
Expand Down
1 change: 1 addition & 0 deletions src/instrumentation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ function getLangfuseProcessorCacheKey(
): string {
return JSON.stringify({
destinationKey,
mediaUploadEnabled: langfuse?.mediaUploadEnabled,
toolOutputTracing: langfuse?.toolOutputTracing,
});
}
Expand Down
9 changes: 9 additions & 0 deletions src/langfuseSpanRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ export function getLangfuseSpanProcessorParams(
secretKey: langfuse.secretKey,
...(isPresent(langfuse.baseUrl) ? { baseUrl: langfuse.baseUrl } : {}),
...(isPresent(environment) ? { environment } : {}),
...(langfuse.mediaUploadEnabled != null
? { mediaUploadEnabled: langfuse.mediaUploadEnabled }
: {}),
};
}
if (hasLangfuseEnvConfig()) {
Expand All @@ -84,6 +87,9 @@ export function getLangfuseSpanProcessorParams(
secretKey: process.env.LANGFUSE_SECRET_KEY as string,
...(isPresent(baseUrl) ? { baseUrl } : {}),
...(isPresent(environment) ? { environment } : {}),
...(langfuse?.mediaUploadEnabled != null
? { mediaUploadEnabled: langfuse.mediaUploadEnabled }
: {}),
};
}
if (isPresent(langfuse?.baseUrl) && hasLangfuseEnvCredentials()) {
Expand All @@ -92,6 +98,9 @@ export function getLangfuseSpanProcessorParams(
secretKey: process.env.LANGFUSE_SECRET_KEY as string,
baseUrl: langfuse.baseUrl,
...(isPresent(environment) ? { environment } : {}),
...(langfuse.mediaUploadEnabled != null
? { mediaUploadEnabled: langfuse.mediaUploadEnabled }
: {}),
};
}
return undefined;
Expand Down
26 changes: 26 additions & 0 deletions src/specs/langfuse-instrumentation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,32 @@ describe('Langfuse instrumentation', () => {
});
});

it('does not reuse processors across different media upload policies', async () => {
const { initializeLangfuseTracing } = await import('@/instrumentation');
initializeLangfuseTracing({
publicKey: 'pk-media',
secretKey: 'sk-media',
baseUrl: 'https://langfuse.media',
mediaUploadEnabled: false,
});
initializeLangfuseTracing({
publicKey: 'pk-media',
secretKey: 'sk-media',
baseUrl: 'https://langfuse.media',
mediaUploadEnabled: true,
});

expect(mockLangfuseSpanProcessor).toHaveBeenCalledTimes(2);
expect(mockLangfuseSpanProcessor).toHaveBeenNthCalledWith(
1,
expect.objectContaining({ mediaUploadEnabled: false })
);
expect(mockLangfuseSpanProcessor).toHaveBeenNthCalledWith(
2,
expect.objectContaining({ mediaUploadEnabled: true })
);
});

it('reuses the isolated provider after initialization', async () => {
process.env.LANGFUSE_SECRET_KEY = 'sk-test';
process.env.LANGFUSE_PUBLIC_KEY = 'pk-test';
Expand Down
17 changes: 17 additions & 0 deletions src/specs/langfuse-span-registry.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Span } from '@opentelemetry/api';
import type * as t from '@/types';
import {
getLangfuseSpanProcessorParams,
getLangfuseManagedSpanDestination,
registerLangfuseManagedSpan,
resolveLangfuseDestinationKey,
Expand Down Expand Up @@ -41,7 +42,23 @@ describe('Langfuse span registry', () => {
const redacting = resolveLangfuseDestinationKey(
tenantConfig({ toolOutputTracing: { enabled: false } })
);
const mediaDisabled = resolveLangfuseDestinationKey(
tenantConfig({ mediaUploadEnabled: false })
);
expect(redacting).toBe(base);
expect(mediaDisabled).toBe(base);
});

it('passes media upload policy to the Langfuse span processor params', () => {
expect(
getLangfuseSpanProcessorParams(
tenantConfig({ mediaUploadEnabled: false })
)
).toEqual(
expect.objectContaining({
mediaUploadEnabled: false,
})
);
});

it('separates destinations by credentials, endpoint, and environment', () => {
Expand Down
44 changes: 44 additions & 0 deletions src/specs/subagent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import * as providers from '@/llm/providers';
import { Run } from '@/run';

const CHILD_RESPONSE = 'Research result: Paris is the capital of France.';
const OVERRIDDEN_CHILD_RESPONSE = 'Deterministic child override result.';

const callerConfig: Partial<RunnableConfig> & {
version: 'v1' | 'v2';
Expand Down Expand Up @@ -226,6 +227,49 @@ describe('Subagent Integration', () => {
expect(subagentTool).toBeDefined();
});

it('only applies an explicitly configured subagent model override', async () => {
const invokeSubagent = async (
overrideSubagents: boolean
): Promise<string> => {
const run = await Run.create<t.IState>({
runId: `subagent-model-override-${overrideSubagents}-${Date.now()}`,
graphConfig: {
type: 'standard',
agents: [createParentAgent()],
},
returnContent: true,
skipCleanup: true,
});
const graph = run.Graph as StandardGraph;
const model = new FakeListChatModel({
responses: [OVERRIDDEN_CHILD_RESPONSE],
});
graph.overrideModel = model;
if (overrideSubagents) {
graph.setSubagentModelOverride(model);
}

const context = graph.agentContexts.get('parent');
const subagentTool = (context?.graphTools as t.GenericTool[]).find(
(tool) => 'name' in tool && tool.name === Constants.SUBAGENT
);
expect(subagentTool).toBeDefined();

return String(
await subagentTool!.invoke(
{
description: 'What is the capital of France?',
subagent_type: 'researcher',
},
callerConfig
)
);
};

await expect(invokeSubagent(false)).resolves.toBe(CHILD_RESPONSE);
await expect(invokeSubagent(true)).resolves.toBe(OVERRIDDEN_CHILD_RESPONSE);
});

it('inherits eager event-tool settings into self-spawn child graphs', async () => {
const originalCreateWorkflow = StandardGraph.prototype.createWorkflow;
const observedChildGraphs: Array<{
Expand Down
7 changes: 5 additions & 2 deletions src/specs/summarization.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1162,7 +1162,7 @@ const hasBedrock = hasEveryEnv(requiredBedrockEnv);

const hasOpenAI = hasEnv('OPENAI_API_KEY');
(hasOpenAI ? describe : describe.skip)('OpenAI Summarization E2E', () => {
jest.setTimeout(120_000);
jest.setTimeout(240_000);

const agentProvider = Providers.OPENAI;
const streamConfig = {
Expand Down Expand Up @@ -1190,6 +1190,9 @@ const hasOpenAI = hasEnv('OPENAI_API_KEY');
agentProvider,
summarizationProvider: Providers.OPENAI,
summarizationModel: 'gpt-4.1-mini',
llmConfigOverride: {
model: 'gpt-4.1-mini',
},
maxContextTokens: maxTokens,
instructions:
'You are a helpful math tutor. Use the calculator tool for ALL computations. Keep responses concise.',
Expand Down Expand Up @@ -1320,7 +1323,7 @@ const hasOpenAI = hasEnv('OPENAI_API_KEY');
` OpenAI summary: "${getSummaryText(completePayload.summary).substring(0, 200)}…"`
);
console.log(` Final messages: ${conversationHistory.length}`);
}, 120_000);
}, 240_000);
});

// ---------------------------------------------------------------------------
Expand Down
Loading
Loading