@librechat/agents 3.2.1 → 3.2.21
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/instrumentation.cjs +33 -0
- package/dist/cjs/instrumentation.cjs.map +1 -1
- package/dist/cjs/main.cjs +3 -0
- package/dist/cjs/main.cjs.map +1 -1
- package/dist/cjs/run.cjs +7 -2
- package/dist/cjs/run.cjs.map +1 -1
- package/dist/esm/instrumentation.mjs +33 -1
- package/dist/esm/instrumentation.mjs.map +1 -1
- package/dist/esm/main.mjs +1 -0
- package/dist/esm/main.mjs.map +1 -1
- package/dist/esm/run.mjs +8 -3
- package/dist/esm/run.mjs.map +1 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/instrumentation.d.ts +1 -0
- package/dist/types/types/graph.d.ts +8 -0
- package/package.json +1 -1
- package/src/index.ts +1 -0
- package/src/instrumentation.ts +41 -0
- package/src/llm/custom-chat-models.smoke.test.ts +114 -0
- package/src/run.ts +19 -11
- package/src/specs/deterministic-trace-id.test.ts +50 -0
- package/src/types/graph.ts +8 -0
|
@@ -135,6 +135,22 @@ type OpenRouterReasoningStreamChoice = Omit<
|
|
|
135
135
|
> & {
|
|
136
136
|
delta: OpenRouterReasoningStreamDelta;
|
|
137
137
|
};
|
|
138
|
+
type OpenAICompatibleReasoningStreamDelta =
|
|
139
|
+
OpenAIClient.Chat.Completions.ChatCompletionChunk.Choice.Delta & {
|
|
140
|
+
reasoning?: string;
|
|
141
|
+
reasoning_details?: Array<{
|
|
142
|
+
type: 'reasoning.text';
|
|
143
|
+
text?: string;
|
|
144
|
+
format?: string;
|
|
145
|
+
index?: number;
|
|
146
|
+
}>;
|
|
147
|
+
};
|
|
148
|
+
type OpenAICompatibleReasoningStreamChoice = Omit<
|
|
149
|
+
OpenAIClient.Chat.Completions.ChatCompletionChunk.Choice,
|
|
150
|
+
'delta'
|
|
151
|
+
> & {
|
|
152
|
+
delta: OpenAICompatibleReasoningStreamDelta;
|
|
153
|
+
};
|
|
138
154
|
type PromptTokensDetailsWithCacheWrite = NonNullable<
|
|
139
155
|
OpenAIClient.Completions.CompletionUsage['prompt_tokens_details']
|
|
140
156
|
> & {
|
|
@@ -472,6 +488,104 @@ describe('custom chat model class smoke tests', () => {
|
|
|
472
488
|
);
|
|
473
489
|
});
|
|
474
490
|
|
|
491
|
+
it('preserves OpenAI-compatible reasoning deltas during OpenAI streaming', async () => {
|
|
492
|
+
const model = new ChatOpenAI({
|
|
493
|
+
model: 'openai/gpt-oss-120b',
|
|
494
|
+
apiKey: 'test-key',
|
|
495
|
+
streaming: true,
|
|
496
|
+
});
|
|
497
|
+
const completions = (model as unknown as StreamingCompletionBackedModel)
|
|
498
|
+
.completions;
|
|
499
|
+
const createChunk = (
|
|
500
|
+
choice: OpenAICompatibleReasoningStreamChoice
|
|
501
|
+
): OpenAIClient.Chat.Completions.ChatCompletionChunk => ({
|
|
502
|
+
id: 'chatcmpl-openai-compatible-reasoning',
|
|
503
|
+
object: 'chat.completion.chunk',
|
|
504
|
+
created: 0,
|
|
505
|
+
model: 'openai/gpt-oss-120b',
|
|
506
|
+
choices: [choice],
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
async function* streamChunks(): AsyncGenerator<OpenAIClient.Chat.Completions.ChatCompletionChunk> {
|
|
510
|
+
yield createChunk({
|
|
511
|
+
index: 0,
|
|
512
|
+
delta: {
|
|
513
|
+
role: 'assistant',
|
|
514
|
+
content: '',
|
|
515
|
+
},
|
|
516
|
+
finish_reason: null,
|
|
517
|
+
});
|
|
518
|
+
yield createChunk({
|
|
519
|
+
index: 0,
|
|
520
|
+
delta: {
|
|
521
|
+
reasoning: 'Think ',
|
|
522
|
+
reasoning_details: [
|
|
523
|
+
{
|
|
524
|
+
type: 'reasoning.text',
|
|
525
|
+
text: 'Think ',
|
|
526
|
+
format: 'text',
|
|
527
|
+
index: 0,
|
|
528
|
+
},
|
|
529
|
+
],
|
|
530
|
+
},
|
|
531
|
+
finish_reason: null,
|
|
532
|
+
});
|
|
533
|
+
yield createChunk({
|
|
534
|
+
index: 0,
|
|
535
|
+
delta: {
|
|
536
|
+
reasoning: 'hard',
|
|
537
|
+
reasoning_details: [
|
|
538
|
+
{
|
|
539
|
+
type: 'reasoning.text',
|
|
540
|
+
text: 'hard',
|
|
541
|
+
format: 'text',
|
|
542
|
+
index: 0,
|
|
543
|
+
},
|
|
544
|
+
],
|
|
545
|
+
},
|
|
546
|
+
finish_reason: null,
|
|
547
|
+
});
|
|
548
|
+
yield createChunk({
|
|
549
|
+
index: 0,
|
|
550
|
+
delta: { content: 'answer' },
|
|
551
|
+
finish_reason: 'stop',
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
completions.completionWithRetry = async (): Promise<
|
|
556
|
+
AsyncIterable<OpenAIClient.Chat.Completions.ChatCompletionChunk>
|
|
557
|
+
> => streamChunks();
|
|
558
|
+
|
|
559
|
+
const chunks: AIMessageChunk[] = [];
|
|
560
|
+
const stream = await model.stream([new HumanMessage('think')]);
|
|
561
|
+
for await (const chunk of stream) {
|
|
562
|
+
chunks.push(chunk);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
expect(
|
|
566
|
+
chunks
|
|
567
|
+
.map((chunk) => chunk.additional_kwargs.reasoning_content)
|
|
568
|
+
.filter((reasoningContent) => reasoningContent != null)
|
|
569
|
+
).toEqual(['Think ', 'hard']);
|
|
570
|
+
expect(chunks[1].additional_kwargs.reasoning_details).toEqual([
|
|
571
|
+
{
|
|
572
|
+
type: 'reasoning.text',
|
|
573
|
+
text: 'Think ',
|
|
574
|
+
format: 'text',
|
|
575
|
+
index: 0,
|
|
576
|
+
},
|
|
577
|
+
]);
|
|
578
|
+
expect(chunks[2].additional_kwargs.reasoning_details).toEqual([
|
|
579
|
+
{
|
|
580
|
+
type: 'reasoning.text',
|
|
581
|
+
text: 'hard',
|
|
582
|
+
format: 'text',
|
|
583
|
+
index: 0,
|
|
584
|
+
},
|
|
585
|
+
]);
|
|
586
|
+
expect(chunks.at(-1)?.content).toBe('answer');
|
|
587
|
+
});
|
|
588
|
+
|
|
475
589
|
it('skips custom OpenAI-compatible SSE events during Azure streaming', async () => {
|
|
476
590
|
await expectCustomSSEEventsSkipped(
|
|
477
591
|
new AzureChatOpenAI({
|
package/src/run.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
// src/run.ts
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
initializeLangfuseTracing,
|
|
4
|
+
runWithTraceIdSeed,
|
|
5
|
+
} from './instrumentation';
|
|
3
6
|
import { PromptTemplate } from '@langchain/core/prompts';
|
|
4
7
|
import { RunnableLambda } from '@langchain/core/runnables';
|
|
5
8
|
import { AzureChatOpenAI, ChatOpenAI } from '@langchain/openai';
|
|
@@ -552,9 +555,7 @@ export class Run<_T extends t.BaseGraphState> {
|
|
|
552
555
|
}
|
|
553
556
|
|
|
554
557
|
private shouldClearHookSession(streamThrew: boolean): boolean {
|
|
555
|
-
return
|
|
556
|
-
this._interrupt == null || this._haltedReason != null || streamThrew
|
|
557
|
-
);
|
|
558
|
+
return this._interrupt == null || this._haltedReason != null || streamThrew;
|
|
558
559
|
}
|
|
559
560
|
|
|
560
561
|
private isAwaitingResume(streamThrew: boolean): boolean {
|
|
@@ -584,9 +585,7 @@ export class Run<_T extends t.BaseGraphState> {
|
|
|
584
585
|
private getStreamToolOutputTracingLangfuseConfig(
|
|
585
586
|
graph: StandardGraph | MultiAgentGraph
|
|
586
587
|
): t.LangfuseConfig | undefined {
|
|
587
|
-
const toolOutputTracingConfigs = Array.from(
|
|
588
|
-
graph.agentContexts.values()
|
|
589
|
-
)
|
|
588
|
+
const toolOutputTracingConfigs = Array.from(graph.agentContexts.values())
|
|
590
589
|
.map((context) => {
|
|
591
590
|
return resolveLangfuseConfig(this.langfuse, context.langfuse)
|
|
592
591
|
?.toolOutputTracing;
|
|
@@ -906,10 +905,19 @@ export class Run<_T extends t.BaseGraphState> {
|
|
|
906
905
|
};
|
|
907
906
|
|
|
908
907
|
try {
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
908
|
+
// When opted in, seed the root trace id from this run's id so feedback /
|
|
909
|
+
// other external signals can be attached to the trace later without a
|
|
910
|
+
// lookup (see SeededTraceIdGenerator in ./instrumentation).
|
|
911
|
+
await runWithTraceIdSeed(
|
|
912
|
+
streamLangfuseConfig?.deterministicTraceId === true
|
|
913
|
+
? this.id
|
|
914
|
+
: undefined,
|
|
915
|
+
() =>
|
|
916
|
+
withLangfuseToolOutputTracingConfig(
|
|
917
|
+
streamLangfuseConfig,
|
|
918
|
+
consumeStream,
|
|
919
|
+
this.getStreamToolOutputTracingLangfuseConfig(graph)
|
|
920
|
+
)
|
|
913
921
|
);
|
|
914
922
|
} catch (err) {
|
|
915
923
|
streamThrew = true;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import {
|
|
3
|
+
initializeLangfuseTracing,
|
|
4
|
+
runWithTraceIdSeed,
|
|
5
|
+
} from '@/instrumentation';
|
|
6
|
+
|
|
7
|
+
const langfuse = {
|
|
8
|
+
publicKey: 'pk-lf-test',
|
|
9
|
+
secretKey: 'sk-lf-test',
|
|
10
|
+
baseUrl: 'http://localhost:3999',
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/** sha256(seed) → first 32 hex chars; what `@langfuse/tracing` `createTraceId` produces. */
|
|
14
|
+
const expectedTraceId = (seed: string): string =>
|
|
15
|
+
createHash('sha256').update(seed, 'utf8').digest('hex').slice(0, 32);
|
|
16
|
+
|
|
17
|
+
describe('deterministic Langfuse trace ids', () => {
|
|
18
|
+
it('derives the root trace id from the seed when run inside runWithTraceIdSeed', () => {
|
|
19
|
+
const provider = initializeLangfuseTracing(langfuse);
|
|
20
|
+
expect(provider).toBeDefined();
|
|
21
|
+
const tracer = provider!.getTracer('deterministic-trace-id-test');
|
|
22
|
+
|
|
23
|
+
const seed = 'response-message-id-1234';
|
|
24
|
+
let traceId: string | undefined;
|
|
25
|
+
runWithTraceIdSeed(seed, () => {
|
|
26
|
+
const span = tracer.startSpan('AgentRun');
|
|
27
|
+
traceId = span.spanContext().traceId;
|
|
28
|
+
span.end();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
expect(traceId).toBe(expectedTraceId(seed));
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('falls back to a random trace id when no seed is active', () => {
|
|
35
|
+
const provider = initializeLangfuseTracing(langfuse);
|
|
36
|
+
const tracer = provider!.getTracer('deterministic-trace-id-test');
|
|
37
|
+
|
|
38
|
+
const span = tracer.startSpan('AgentRun');
|
|
39
|
+
const traceId = span.spanContext().traceId;
|
|
40
|
+
span.end();
|
|
41
|
+
|
|
42
|
+
expect(traceId).toMatch(/^[0-9a-f]{32}$/);
|
|
43
|
+
expect(traceId).not.toBe(expectedTraceId('response-message-id-1234'));
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('runWithTraceIdSeed is a passthrough when the seed is undefined', () => {
|
|
47
|
+
const sentinel = {};
|
|
48
|
+
expect(runWithTraceIdSeed(undefined, () => sentinel)).toBe(sentinel);
|
|
49
|
+
});
|
|
50
|
+
});
|
package/src/types/graph.ts
CHANGED
|
@@ -445,6 +445,14 @@ export interface LangfuseConfig {
|
|
|
445
445
|
baseUrl?: string;
|
|
446
446
|
toolNodeTracing?: LangfuseToolNodeTracingConfig;
|
|
447
447
|
toolOutputTracing?: LangfuseToolOutputTracingConfig;
|
|
448
|
+
/**
|
|
449
|
+
* When true, derive the run's root Langfuse trace id deterministically from
|
|
450
|
+
* its `runId` (`sha256(runId)` → 32 hex chars, matching `@langfuse/tracing`
|
|
451
|
+
* `createTraceId`) instead of a random id. This lets external systems attach
|
|
452
|
+
* scores or observations to the trace afterwards by regenerating the same id
|
|
453
|
+
* from the run/message id, without a trace lookup. Default: random ids.
|
|
454
|
+
*/
|
|
455
|
+
deterministicTraceId?: boolean;
|
|
448
456
|
}
|
|
449
457
|
|
|
450
458
|
export interface AgentInputs {
|