@librechat/agents 4.0.1 → 4.0.3
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 +3 -2
- package/dist/cjs/agents/AgentContext.cjs.map +1 -1
- package/dist/cjs/decisions/deadline.cjs +52 -0
- package/dist/cjs/decisions/deadline.cjs.map +1 -0
- package/dist/cjs/decisions/dialect.cjs +88 -0
- package/dist/cjs/decisions/dialect.cjs.map +1 -0
- package/dist/cjs/decisions/http.cjs +77 -0
- package/dist/cjs/decisions/http.cjs.map +1 -0
- package/dist/cjs/decisions/index.cjs +7 -0
- package/dist/cjs/decisions/presets.cjs +82 -0
- package/dist/cjs/decisions/presets.cjs.map +1 -0
- package/dist/cjs/decisions/questions.cjs +70 -0
- package/dist/cjs/decisions/questions.cjs.map +1 -0
- package/dist/cjs/decisions/structuredChat.cjs +172 -0
- package/dist/cjs/decisions/structuredChat.cjs.map +1 -0
- package/dist/cjs/decisions/structuredOutput.cjs +18 -0
- package/dist/cjs/decisions/structuredOutput.cjs.map +1 -0
- package/dist/cjs/decisions/traceMarker.cjs +6 -0
- package/dist/cjs/decisions/traceMarker.cjs.map +1 -0
- package/dist/cjs/decisions/transport.cjs +166 -0
- package/dist/cjs/decisions/transport.cjs.map +1 -0
- package/dist/cjs/decisions/types.cjs +44 -0
- package/dist/cjs/decisions/types.cjs.map +1 -0
- package/dist/cjs/graphs/Graph.cjs +28 -9
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/langfuseToolOutputTracing.cjs +6 -1
- package/dist/cjs/langfuseToolOutputTracing.cjs.map +1 -1
- package/dist/cjs/llm/google/index.cjs +1 -1
- package/dist/cjs/llm/invoke.cjs +34 -12
- package/dist/cjs/llm/invoke.cjs.map +1 -1
- package/dist/cjs/llm/openai/index.cjs +7 -0
- package/dist/cjs/llm/openai/index.cjs.map +1 -1
- package/dist/cjs/llm/providerTextBoundary.cjs +355 -0
- package/dist/cjs/llm/providerTextBoundary.cjs.map +1 -0
- package/dist/cjs/llm/providerTextChunk.cjs +14 -0
- package/dist/cjs/llm/providerTextChunk.cjs.map +1 -0
- package/dist/cjs/llm/providerTextControls.cjs +236 -0
- package/dist/cjs/llm/providerTextControls.cjs.map +1 -0
- package/dist/cjs/llm/stream/smoother.cjs +2 -1
- package/dist/cjs/llm/stream/smoother.cjs.map +1 -1
- package/dist/cjs/main.cjs +49 -1
- package/dist/cjs/protection/providerText.cjs +224 -0
- package/dist/cjs/protection/providerText.cjs.map +1 -0
- package/dist/cjs/protection/providerTextInput.cjs +25 -0
- package/dist/cjs/protection/providerTextInput.cjs.map +1 -0
- package/dist/cjs/rerank/index.cjs +2 -0
- package/dist/cjs/rerank/search.cjs +45 -0
- package/dist/cjs/rerank/search.cjs.map +1 -0
- package/dist/cjs/rerank/systemone.cjs +70 -0
- package/dist/cjs/rerank/systemone.cjs.map +1 -0
- package/dist/cjs/run.cjs +11 -1
- package/dist/cjs/run.cjs.map +1 -1
- package/dist/cjs/stream.cjs +42 -14
- package/dist/cjs/stream.cjs.map +1 -1
- package/dist/cjs/summarization/node.cjs +26 -8
- package/dist/cjs/summarization/node.cjs.map +1 -1
- package/dist/cjs/tools/SubagentTool.cjs +11 -3
- package/dist/cjs/tools/SubagentTool.cjs.map +1 -1
- package/dist/cjs/tools/ToolNode.cjs +6 -5
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/cjs/tools/search/rerankers.cjs +1 -0
- package/dist/cjs/tools/search/search.cjs +3 -3
- package/dist/cjs/tools/search/search.cjs.map +1 -1
- package/dist/cjs/tools/search/tool.cjs +4 -3
- package/dist/cjs/tools/search/tool.cjs.map +1 -1
- package/dist/cjs/tools/subagent/SubagentExecutionRegistry.cjs +13 -2
- package/dist/cjs/tools/subagent/SubagentExecutionRegistry.cjs.map +1 -1
- package/dist/cjs/tools/subagent/SubagentExecutor.cjs +149 -26
- package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
- package/dist/cjs/tools/subagent/SubagentReplay.cjs +4 -2
- package/dist/cjs/tools/subagent/SubagentReplay.cjs.map +1 -1
- package/dist/cjs/tools/subagent/childGraphConfig.cjs +3 -1
- package/dist/cjs/tools/subagent/childGraphConfig.cjs.map +1 -1
- package/dist/cjs/tools/subagent/diagnostics.cjs +88 -2
- package/dist/cjs/tools/subagent/diagnostics.cjs.map +1 -1
- package/dist/cjs/tools/subagent/hostArgs.cjs +214 -0
- package/dist/cjs/tools/subagent/hostArgs.cjs.map +1 -0
- package/dist/cjs/tools/subagent/index.cjs +2 -0
- package/dist/cjs/tools/toolErrorContent.cjs +1 -1
- package/dist/esm/agents/AgentContext.mjs +3 -2
- package/dist/esm/agents/AgentContext.mjs.map +1 -1
- package/dist/esm/decisions/deadline.mjs +52 -0
- package/dist/esm/decisions/deadline.mjs.map +1 -0
- package/dist/esm/decisions/dialect.mjs +87 -0
- package/dist/esm/decisions/dialect.mjs.map +1 -0
- package/dist/esm/decisions/http.mjs +75 -0
- package/dist/esm/decisions/http.mjs.map +1 -0
- package/dist/esm/decisions/index.mjs +8 -0
- package/dist/esm/decisions/presets.mjs +77 -0
- package/dist/esm/decisions/presets.mjs.map +1 -0
- package/dist/esm/decisions/questions.mjs +67 -0
- package/dist/esm/decisions/questions.mjs.map +1 -0
- package/dist/esm/decisions/structuredChat.mjs +172 -0
- package/dist/esm/decisions/structuredChat.mjs.map +1 -0
- package/dist/esm/decisions/structuredOutput.mjs +18 -0
- package/dist/esm/decisions/structuredOutput.mjs.map +1 -0
- package/dist/esm/decisions/traceMarker.mjs +6 -0
- package/dist/esm/decisions/traceMarker.mjs.map +1 -0
- package/dist/esm/decisions/transport.mjs +164 -0
- package/dist/esm/decisions/transport.mjs.map +1 -0
- package/dist/esm/decisions/types.mjs +39 -0
- package/dist/esm/decisions/types.mjs.map +1 -0
- package/dist/esm/graphs/Graph.mjs +28 -9
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/langfuseToolOutputTracing.mjs +6 -1
- package/dist/esm/langfuseToolOutputTracing.mjs.map +1 -1
- package/dist/esm/llm/google/index.mjs +1 -1
- package/dist/esm/llm/invoke.mjs +34 -12
- package/dist/esm/llm/invoke.mjs.map +1 -1
- package/dist/esm/llm/openai/index.mjs +7 -0
- package/dist/esm/llm/openai/index.mjs.map +1 -1
- package/dist/esm/llm/providerTextBoundary.mjs +355 -0
- package/dist/esm/llm/providerTextBoundary.mjs.map +1 -0
- package/dist/esm/llm/providerTextChunk.mjs +13 -0
- package/dist/esm/llm/providerTextChunk.mjs.map +1 -0
- package/dist/esm/llm/providerTextControls.mjs +230 -0
- package/dist/esm/llm/providerTextControls.mjs.map +1 -0
- package/dist/esm/llm/stream/smoother.mjs +2 -1
- package/dist/esm/llm/stream/smoother.mjs.map +1 -1
- package/dist/esm/main.mjs +16 -2
- package/dist/esm/protection/providerText.mjs +218 -0
- package/dist/esm/protection/providerText.mjs.map +1 -0
- package/dist/esm/protection/providerTextInput.mjs +24 -0
- package/dist/esm/protection/providerTextInput.mjs.map +1 -0
- package/dist/esm/rerank/index.mjs +3 -0
- package/dist/esm/rerank/search.mjs +45 -0
- package/dist/esm/rerank/search.mjs.map +1 -0
- package/dist/esm/rerank/systemone.mjs +70 -0
- package/dist/esm/rerank/systemone.mjs.map +1 -0
- package/dist/esm/run.mjs +11 -1
- package/dist/esm/run.mjs.map +1 -1
- package/dist/esm/stream.mjs +42 -14
- package/dist/esm/stream.mjs.map +1 -1
- package/dist/esm/summarization/node.mjs +26 -8
- package/dist/esm/summarization/node.mjs.map +1 -1
- package/dist/esm/tools/SubagentTool.mjs +11 -3
- package/dist/esm/tools/SubagentTool.mjs.map +1 -1
- package/dist/esm/tools/ToolNode.mjs +6 -5
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/esm/tools/search/rerankers.mjs +1 -1
- package/dist/esm/tools/search/search.mjs +3 -3
- package/dist/esm/tools/search/search.mjs.map +1 -1
- package/dist/esm/tools/search/tool.mjs +4 -3
- package/dist/esm/tools/search/tool.mjs.map +1 -1
- package/dist/esm/tools/subagent/SubagentExecutionRegistry.mjs +13 -2
- package/dist/esm/tools/subagent/SubagentExecutionRegistry.mjs.map +1 -1
- package/dist/esm/tools/subagent/SubagentExecutor.mjs +149 -26
- package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
- package/dist/esm/tools/subagent/SubagentReplay.mjs +4 -2
- package/dist/esm/tools/subagent/SubagentReplay.mjs.map +1 -1
- package/dist/esm/tools/subagent/childGraphConfig.mjs +3 -1
- package/dist/esm/tools/subagent/childGraphConfig.mjs.map +1 -1
- package/dist/esm/tools/subagent/diagnostics.mjs +84 -3
- package/dist/esm/tools/subagent/diagnostics.mjs.map +1 -1
- package/dist/esm/tools/subagent/hostArgs.mjs +207 -0
- package/dist/esm/tools/subagent/hostArgs.mjs.map +1 -0
- package/dist/esm/tools/subagent/index.mjs +2 -0
- package/dist/esm/tools/toolErrorContent.mjs +1 -1
- package/dist/types/decisions/deadline.d.ts +3 -0
- package/dist/types/decisions/dialect.d.ts +11 -0
- package/dist/types/decisions/http.d.ts +22 -0
- package/dist/types/decisions/index.d.ts +10 -0
- package/dist/types/decisions/presets.d.ts +17 -0
- package/dist/types/decisions/questions.d.ts +8 -0
- package/dist/types/decisions/structuredChat.d.ts +17 -0
- package/dist/types/decisions/structuredOutput.d.ts +4 -0
- package/dist/types/decisions/traceMarker.d.ts +2 -0
- package/dist/types/decisions/transport.d.ts +29 -0
- package/dist/types/decisions/types.d.ts +119 -0
- package/dist/types/graphs/Graph.d.ts +7 -3
- package/dist/types/index.d.ts +4 -0
- package/dist/types/llm/invoke.d.ts +4 -1
- package/dist/types/llm/providerTextBoundary.d.ts +4 -0
- package/dist/types/llm/providerTextChunk.d.ts +7 -0
- package/dist/types/llm/providerTextControls.d.ts +18 -0
- package/dist/types/protection/providerText.d.ts +77 -0
- package/dist/types/protection/providerTextInput.d.ts +3 -0
- package/dist/types/rerank/index.d.ts +4 -0
- package/dist/types/rerank/search.d.ts +7 -0
- package/dist/types/rerank/systemone.d.ts +13 -0
- package/dist/types/rerank/types.d.ts +24 -0
- package/dist/types/run.d.ts +2 -0
- package/dist/types/tools/BashExecutor.d.ts +4 -0
- package/dist/types/tools/CodeExecutor.d.ts +4 -0
- package/dist/types/tools/ReadFile.d.ts +4 -0
- package/dist/types/tools/SkillTool.d.ts +4 -0
- package/dist/types/tools/SubagentTool.d.ts +2 -0
- package/dist/types/tools/ToolSearch.d.ts +4 -0
- package/dist/types/tools/search/schema.d.ts +4 -0
- package/dist/types/tools/search/types.d.ts +3 -1
- package/dist/types/tools/subagent/SubagentExecutionRegistry.d.ts +6 -0
- package/dist/types/tools/subagent/SubagentExecutor.d.ts +22 -1
- package/dist/types/tools/subagent/SubagentReplay.d.ts +3 -1
- package/dist/types/tools/subagent/diagnostics.d.ts +56 -8
- package/dist/types/tools/subagent/hostArgs.d.ts +57 -0
- package/dist/types/tools/subagent/index.d.ts +3 -0
- package/dist/types/types/graph.d.ts +39 -0
- package/dist/types/types/run.d.ts +6 -2
- package/dist/types/types/tools.d.ts +2 -0
- package/package.json +4 -4
- package/src/agents/AgentContext.ts +3 -2
- package/src/decisions/deadline.ts +92 -0
- package/src/decisions/dialect.ts +200 -0
- package/src/decisions/http.ts +175 -0
- package/src/decisions/index.ts +14 -0
- package/src/decisions/presets.ts +111 -0
- package/src/decisions/questions.ts +128 -0
- package/src/decisions/structuredChat.ts +358 -0
- package/src/decisions/structuredOutput.ts +25 -0
- package/src/decisions/traceMarker.ts +2 -0
- package/src/decisions/transport.ts +335 -0
- package/src/decisions/types.ts +193 -0
- package/src/graphs/Graph.ts +57 -9
- package/src/index.ts +9 -0
- package/src/langfuseToolOutputTracing.ts +8 -1
- package/src/llm/invoke.ts +48 -7
- package/src/llm/openai/index.ts +8 -0
- package/src/llm/providerTextBoundary.ts +446 -0
- package/src/llm/providerTextChunk.ts +15 -0
- package/src/llm/providerTextControls.ts +173 -0
- package/src/llm/stream/smoother.ts +4 -2
- package/src/protection/providerText.ts +325 -0
- package/src/protection/providerTextInput.ts +21 -0
- package/src/rerank/index.ts +4 -0
- package/src/rerank/search.ts +85 -0
- package/src/rerank/systemone.ts +140 -0
- package/src/rerank/types.ts +28 -0
- package/src/run.ts +13 -0
- package/src/stream.ts +33 -7
- package/src/summarization/node.ts +32 -14
- package/src/tools/SubagentTool.ts +14 -3
- package/src/tools/ToolNode.ts +7 -3
- package/src/tools/search/search.ts +4 -4
- package/src/tools/search/tool.ts +27 -25
- package/src/tools/search/types.ts +4 -1
- package/src/tools/subagent/SubagentExecutionRegistry.ts +27 -2
- package/src/tools/subagent/SubagentExecutor.ts +323 -28
- package/src/tools/subagent/SubagentReplay.ts +10 -3
- package/src/tools/subagent/childGraphConfig.ts +12 -1
- package/src/tools/subagent/diagnostics.ts +180 -9
- package/src/tools/subagent/hostArgs.ts +413 -0
- package/src/tools/subagent/index.ts +20 -0
- package/src/types/graph.ts +44 -0
- package/src/types/run.ts +6 -2
- package/src/types/tools.ts +6 -1
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
import { ChatGenerationChunk } from '@langchain/core/outputs';
|
|
2
|
+
import { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
|
3
|
+
import { RunnableBinding, RunnableSequence } from '@langchain/core/runnables';
|
|
4
|
+
import { AIMessage, AIMessageChunk, mergeContent } from '@langchain/core/messages';
|
|
5
|
+
import type { MessageContentComplex } from '@langchain/core/messages';
|
|
6
|
+
import type { ChatResult } from '@langchain/core/outputs';
|
|
7
|
+
import type { ProviderTextProtection } from '@/protection/providerText';
|
|
8
|
+
import type { ChatModel, ProviderName } from '@/types';
|
|
9
|
+
import {
|
|
10
|
+
getStreamedToolCallAdapter,
|
|
11
|
+
getStreamedToolCallSeal,
|
|
12
|
+
STREAMED_TOOL_CALL_ADAPTER_METADATA_KEY,
|
|
13
|
+
STREAMED_TOOL_CALL_SEAL_METADATA_KEY,
|
|
14
|
+
} from '@/tools/streamedToolCallSeals';
|
|
15
|
+
import { GEMINI_SIGNATURES, RESPONSES_POSITIONS, stringMap, safetyRatings, replayPositions, ResponsesTextProjection, keys } from '@/llm/providerTextControls';
|
|
16
|
+
import {
|
|
17
|
+
ProviderTextAttempt,
|
|
18
|
+
ProviderTextProtectionError,
|
|
19
|
+
} from '@/protection/providerText';
|
|
20
|
+
import { isProviderTextInput } from '@/protection/providerTextInput';
|
|
21
|
+
import { createProviderTextChunk } from '@/llm/providerTextChunk';
|
|
22
|
+
import { getChatModelClass } from '@/llm/providers';
|
|
23
|
+
import { Providers } from '@/common';
|
|
24
|
+
|
|
25
|
+
const additionalKeys = new Set([
|
|
26
|
+
'tool_calls',
|
|
27
|
+
'function_call',
|
|
28
|
+
'reasoning',
|
|
29
|
+
'reasoning_content',
|
|
30
|
+
'reasoning_details',
|
|
31
|
+
'thinking',
|
|
32
|
+
'signature',
|
|
33
|
+
]);
|
|
34
|
+
const metadataKeys = new Set([
|
|
35
|
+
'finish_reason', 'stop_reason', 'stop_sequence', 'model_name', 'model', 'model_provider',
|
|
36
|
+
'system_fingerprint', 'service_tier', 'usage', 'tokenUsage', 'input_tokens', 'output_tokens',
|
|
37
|
+
'total_tokens', 'index', 'prompt', 'completion', 'output_version',
|
|
38
|
+
]);
|
|
39
|
+
const additionalControls: Readonly<Partial<Record<string, (value: unknown) => boolean>>> = {
|
|
40
|
+
id: (value) => typeof value === 'string',
|
|
41
|
+
type: (value) => value === 'message',
|
|
42
|
+
role: (value) => value === 'assistant',
|
|
43
|
+
model: (value) => typeof value === 'string',
|
|
44
|
+
stop_reason: (value) => value == null || typeof value === 'string',
|
|
45
|
+
stop_sequence: (value) => value == null || typeof value === 'string',
|
|
46
|
+
usage: (value) => numericMetadata(value),
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const geminiControls: Readonly<Partial<Record<string, (value: unknown) => boolean>>> = {
|
|
50
|
+
[GEMINI_SIGNATURES]: stringMap,
|
|
51
|
+
finishReason: (value) => ['STOP', 'MAX_TOKENS', 'SAFETY', 'RECITATION', 'OTHER', 'MALFORMED_FUNCTION_CALL', 'FINISH_REASON_UNSPECIFIED'].includes(String(value)),
|
|
52
|
+
safetyRatings,
|
|
53
|
+
avgLogprobs: (value) => typeof value === 'number' && Number.isFinite(value),
|
|
54
|
+
index: (value) => typeof value === 'number' && Number.isSafeInteger(value) && value >= 0,
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const controlBlocks = new Set([
|
|
58
|
+
'thinking',
|
|
59
|
+
'redacted_thinking',
|
|
60
|
+
'reasoning',
|
|
61
|
+
'reasoning_content',
|
|
62
|
+
'tool_use',
|
|
63
|
+
]);
|
|
64
|
+
|
|
65
|
+
type TextBlock = MessageContentComplex & {
|
|
66
|
+
type: 'text';
|
|
67
|
+
text: string;
|
|
68
|
+
index?: number;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
function copyMessage(
|
|
72
|
+
message: AIMessage | AIMessageChunk,
|
|
73
|
+
content: AIMessageChunk['content']
|
|
74
|
+
): AIMessageChunk {
|
|
75
|
+
const copied = createProviderTextChunk(message, {
|
|
76
|
+
content,
|
|
77
|
+
id: message.id,
|
|
78
|
+
name: message.name,
|
|
79
|
+
additional_kwargs: message.additional_kwargs,
|
|
80
|
+
response_metadata: message.response_metadata,
|
|
81
|
+
tool_calls: message.tool_calls,
|
|
82
|
+
tool_call_chunks: message instanceof AIMessageChunk ? message.tool_call_chunks : undefined,
|
|
83
|
+
invalid_tool_calls: message.invalid_tool_calls,
|
|
84
|
+
usage_metadata: message.usage_metadata,
|
|
85
|
+
});
|
|
86
|
+
// Core resets native diagnostics when no argument chunks are present.
|
|
87
|
+
copied.invalid_tool_calls = message.invalid_tool_calls ?? [];
|
|
88
|
+
copied.lc_kwargs.invalid_tool_calls = copied.invalid_tool_calls;
|
|
89
|
+
return copied;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function numericMetadata(value: unknown, depth = 4): boolean {
|
|
93
|
+
if (value == null) return true;
|
|
94
|
+
if (typeof value === 'number') return Number.isFinite(value);
|
|
95
|
+
if (typeof value !== 'object' || Array.isArray(value) || depth === 0) return false;
|
|
96
|
+
const values = Object.values(value);
|
|
97
|
+
return values.length <= 64 && values.every((entry) => numericMetadata(entry, depth - 1));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function hasKeys(value: unknown, keys: readonly string[]): value is Record<string, unknown> {
|
|
101
|
+
return value != null && typeof value === 'object' && !Array.isArray(value) &&
|
|
102
|
+
Object.keys(value).every((key) => keys.includes(key));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function validateMetadata(metadata: AIMessageChunk['response_metadata'], google = false): void {
|
|
106
|
+
for (const key of Object.keys(metadata)) {
|
|
107
|
+
const value: unknown = metadata[key];
|
|
108
|
+
let valid: boolean;
|
|
109
|
+
if (google && geminiControls[key] != null) {
|
|
110
|
+
valid = geminiControls[key](value);
|
|
111
|
+
} else if (additionalControls[key] != null) {
|
|
112
|
+
valid = additionalControls[key](value);
|
|
113
|
+
} else if (key === 'contentBlockIndex') {
|
|
114
|
+
valid = typeof value === 'number' && Number.isSafeInteger(value) && value >= 0;
|
|
115
|
+
} else if (key === STREAMED_TOOL_CALL_ADAPTER_METADATA_KEY) {
|
|
116
|
+
valid = getStreamedToolCallAdapter(metadata) != null;
|
|
117
|
+
} else if (key === STREAMED_TOOL_CALL_SEAL_METADATA_KEY) {
|
|
118
|
+
valid = hasKeys(value, ['kind', 'id', 'index']) && getStreamedToolCallSeal(metadata) != null;
|
|
119
|
+
} else if (key === 'messageStart') {
|
|
120
|
+
valid = hasKeys(value, ['role']) && value.role === 'assistant';
|
|
121
|
+
} else if (key === 'messageStop') {
|
|
122
|
+
valid = hasKeys(value, ['stopReason']) && typeof value.stopReason === 'string';
|
|
123
|
+
} else if (key === 'metadata') {
|
|
124
|
+
valid = hasKeys(value, ['usage', 'metrics']) && numericMetadata(value);
|
|
125
|
+
} else if (key === 'usage' || key === 'tokenUsage' || key === 'estimatedTokenUsage') {
|
|
126
|
+
valid = numericMetadata(value);
|
|
127
|
+
} else {
|
|
128
|
+
valid = metadataKeys.has(key) && (value == null || typeof value === 'string' ||
|
|
129
|
+
(typeof value === 'number' && Number.isFinite(value)));
|
|
130
|
+
}
|
|
131
|
+
if (!valid) throw new ProviderTextProtectionError('unsupported');
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
class Candidate {
|
|
136
|
+
private block?: TextBlock;
|
|
137
|
+
private stringContent = false;
|
|
138
|
+
private readonly toolInputIndices = new Set<number>();
|
|
139
|
+
private readonly responses: ResponsesTextProjection;
|
|
140
|
+
|
|
141
|
+
constructor(private readonly attempt: ProviderTextAttempt, private readonly provider?: ProviderName) { this.responses = new ResponsesTextProjection(attempt); }
|
|
142
|
+
|
|
143
|
+
strip(generation: ChatGenerationChunk): ChatGenerationChunk {
|
|
144
|
+
const message = generation.message;
|
|
145
|
+
if (!(message instanceof AIMessageChunk))
|
|
146
|
+
throw new ProviderTextProtectionError('unsupported');
|
|
147
|
+
this.attempt.observeChunk();
|
|
148
|
+
for (const key of Object.keys(message.additional_kwargs)) {
|
|
149
|
+
if (additionalKeys.has(key)) continue;
|
|
150
|
+
const validate = (this.provider === Providers.GOOGLE ? geminiControls[key] : undefined) ??
|
|
151
|
+
(key === RESPONSES_POSITIONS ? replayPositions : undefined) ??
|
|
152
|
+
(key === '__openai_function_call_ids__' ? stringMap : undefined) ?? additionalControls[key];
|
|
153
|
+
if (validate == null || !validate(message.additional_kwargs[key])) {
|
|
154
|
+
throw new ProviderTextProtectionError('unsupported');
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
let content: AIMessageChunk['content'];
|
|
158
|
+
if (typeof message.content === 'string') {
|
|
159
|
+
if (message.content && this.block != null)
|
|
160
|
+
throw new ProviderTextProtectionError('unsupported');
|
|
161
|
+
this.stringContent ||= message.content.length > 0;
|
|
162
|
+
const details = message.additional_kwargs.reasoning_details;
|
|
163
|
+
this.attempt.append(message.content, this.provider === Providers.OPENROUTER && Array.isArray(details) && details.length > 0);
|
|
164
|
+
content = '';
|
|
165
|
+
} else {
|
|
166
|
+
content = [];
|
|
167
|
+
for (const block of message.content) {
|
|
168
|
+
if (typeof block === 'string')
|
|
169
|
+
throw new ProviderTextProtectionError('unsupported');
|
|
170
|
+
if (block.type !== 'text') {
|
|
171
|
+
this.admitControl(block, message);
|
|
172
|
+
content.push(block);
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
if (
|
|
176
|
+
typeof block.text !== 'string' ||
|
|
177
|
+
this.stringContent ||
|
|
178
|
+
Object.keys(block).some(
|
|
179
|
+
(key) => !['type', 'text', 'index', 'annotations', 'phase'].includes(key)
|
|
180
|
+
) ||
|
|
181
|
+
('annotations' in block && (!Array.isArray(block.annotations) || block.annotations.length > 0)) ||
|
|
182
|
+
('phase' in block && block.phase != null && !['commentary', 'final_answer'].includes(String(block.phase)))
|
|
183
|
+
) {
|
|
184
|
+
throw new ProviderTextProtectionError('unsupported');
|
|
185
|
+
}
|
|
186
|
+
if (this.block != null && this.block.index !== block.index) {
|
|
187
|
+
throw new ProviderTextProtectionError('unsupported');
|
|
188
|
+
}
|
|
189
|
+
this.block ??= { ...block, text: '' } as TextBlock;
|
|
190
|
+
this.attempt.append(block.text);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
const metadata = this.responses.strip(message.response_metadata);
|
|
194
|
+
if (!Object.hasOwn(message.response_metadata, 'output')) validateMetadata(metadata, this.provider === Providers.GOOGLE);
|
|
195
|
+
validateMetadata(generation.generationInfo ?? {}, this.provider === Providers.GOOGLE);
|
|
196
|
+
const stripped = copyMessage(message, content);
|
|
197
|
+
stripped.response_metadata = metadata;
|
|
198
|
+
stripped.lc_kwargs.response_metadata = metadata;
|
|
199
|
+
return new ChatGenerationChunk({ text: '', message: stripped, generationInfo: generation.generationInfo });
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
private admitControl(block: MessageContentComplex, message: AIMessageChunk): void {
|
|
203
|
+
const control: { type?: string; index?: unknown; input?: unknown; id?: unknown; name?: unknown } = block;
|
|
204
|
+
const { type, index, input, id, name } = control;
|
|
205
|
+
const part: unknown = block;
|
|
206
|
+
if (this.provider === Providers.GOOGLE && keys(part, ['functionCall', 'thoughtSignature']) &&
|
|
207
|
+
keys(part.functionCall, ['name', 'args', 'id'])) {
|
|
208
|
+
const call = part.functionCall;
|
|
209
|
+
if (typeof call.name !== 'string' || (part.thoughtSignature != null && typeof part.thoughtSignature !== 'string') ||
|
|
210
|
+
message.tool_calls?.some((tool) => tool.name === call.name && JSON.stringify(tool.args) === JSON.stringify(call.args)) !== true) {
|
|
211
|
+
throw new ProviderTextProtectionError('unsupported');
|
|
212
|
+
}
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
if (!Object.hasOwn(block, 'type')) {
|
|
216
|
+
if (!hasKeys(block, ['index', 'input']) || typeof index !== 'number' ||
|
|
217
|
+
!Number.isSafeInteger(index) || index < 0 || typeof input !== 'string' ||
|
|
218
|
+
!this.toolInputIndices.has(index) ||
|
|
219
|
+
message.tool_call_chunks?.some((call) => call.index === index && call.args === input) !== true) {
|
|
220
|
+
throw new ProviderTextProtectionError('unsupported');
|
|
221
|
+
}
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
if (type == null || !controlBlocks.has(type)) throw new ProviderTextProtectionError('unsupported');
|
|
225
|
+
if (type === 'tool_use' && index != null) {
|
|
226
|
+
if (typeof index !== 'number' || !Number.isSafeInteger(index) || index < 0 ||
|
|
227
|
+
message.tool_call_chunks?.some((call) => call.index === index && call.id === id && call.name === name) !== true) {
|
|
228
|
+
throw new ProviderTextProtectionError('unsupported');
|
|
229
|
+
}
|
|
230
|
+
if (!this.toolInputIndices.has(index)) {
|
|
231
|
+
this.attempt.observeChunk();
|
|
232
|
+
this.toolInputIndices.add(index);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async canonical(): Promise<ChatGenerationChunk | undefined> {
|
|
238
|
+
const content = await this.attempt.release();
|
|
239
|
+
const metadata = this.responses.canonical(content);
|
|
240
|
+
if (!content && metadata == null) return undefined;
|
|
241
|
+
return new ChatGenerationChunk({
|
|
242
|
+
text: content,
|
|
243
|
+
message: new AIMessageChunk({
|
|
244
|
+
content:
|
|
245
|
+
this.block == null ? content : [{ ...this.block, text: content }],
|
|
246
|
+
response_metadata: metadata,
|
|
247
|
+
}),
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function clone<T extends object>(value: T): T {
|
|
253
|
+
return Object.create(
|
|
254
|
+
Object.getPrototypeOf(value),
|
|
255
|
+
Object.getOwnPropertyDescriptors(value)
|
|
256
|
+
) as T;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function assertBindingShell(model: RunnableBinding<never, unknown>): void {
|
|
260
|
+
if (model.constructor !== RunnableBinding || (model.configFactories?.length ?? 0) > 0 ||
|
|
261
|
+
model.invoke !== RunnableBinding.prototype.invoke || model.batch !== RunnableBinding.prototype.batch ||
|
|
262
|
+
model.stream !== RunnableBinding.prototype.stream || model.transform !== RunnableBinding.prototype.transform ||
|
|
263
|
+
model._streamIterator !== RunnableBinding.prototype._streamIterator) {
|
|
264
|
+
throw new ProviderTextProtectionError('unsupported');
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function assertInputStep(input: object): void {
|
|
269
|
+
if (isProviderTextInput(input)) return;
|
|
270
|
+
if (input instanceof RunnableBinding) {
|
|
271
|
+
assertBindingShell(input);
|
|
272
|
+
assertInputStep(input.bound);
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
throw new ProviderTextProtectionError('unsupported');
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function hasStreamingInvocation(
|
|
279
|
+
model: BaseChatModel,
|
|
280
|
+
options?: BaseChatModel['ParsedCallOptions']
|
|
281
|
+
): boolean {
|
|
282
|
+
if ('streaming' in model && model.streaming === true) return true;
|
|
283
|
+
const params: unknown = model.invocationParams(options);
|
|
284
|
+
if (params == null || typeof params !== 'object' || Array.isArray(params)) {
|
|
285
|
+
throw new ProviderTextProtectionError('unsupported');
|
|
286
|
+
}
|
|
287
|
+
return 'stream' in params && params.stream != null && params.stream !== false;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function usesInternalStreaming(
|
|
291
|
+
model: BaseChatModel,
|
|
292
|
+
options?: BaseChatModel['ParsedCallOptions']
|
|
293
|
+
): boolean {
|
|
294
|
+
if (hasStreamingInvocation(model, options)) return true;
|
|
295
|
+
for (const key of ['completions', 'responses'] as const) {
|
|
296
|
+
const delegate: unknown = Reflect.get(model, key);
|
|
297
|
+
if (delegate instanceof BaseChatModel && hasStreamingInvocation(delegate, options)) return true;
|
|
298
|
+
}
|
|
299
|
+
return false;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** Clone only known runnable shells. Never mutate a shared provider or its callback configuration. */
|
|
303
|
+
export function withProviderTextBoundary(
|
|
304
|
+
model: ChatModel,
|
|
305
|
+
policy: ProviderTextProtection,
|
|
306
|
+
provider?: ProviderName
|
|
307
|
+
): ChatModel {
|
|
308
|
+
const protectedModel = clone(model);
|
|
309
|
+
if (model instanceof RunnableBinding) {
|
|
310
|
+
assertBindingShell(model);
|
|
311
|
+
Object.defineProperty(protectedModel, 'bound', {
|
|
312
|
+
value: withProviderTextBoundary(model.bound as ChatModel, policy, provider),
|
|
313
|
+
});
|
|
314
|
+
return protectedModel;
|
|
315
|
+
}
|
|
316
|
+
if (model instanceof RunnableSequence) {
|
|
317
|
+
if (model.constructor !== RunnableSequence ||
|
|
318
|
+
model.invoke !== RunnableSequence.prototype.invoke || model.batch !== RunnableSequence.prototype.batch ||
|
|
319
|
+
model.stream !== RunnableSequence.prototype.stream || model.transform !== RunnableSequence.prototype.transform ||
|
|
320
|
+
model._streamIterator !== RunnableSequence.prototype._streamIterator) {
|
|
321
|
+
throw new ProviderTextProtectionError('unsupported');
|
|
322
|
+
}
|
|
323
|
+
const steps = model.steps;
|
|
324
|
+
for (let index = 0; index < steps.length - 1; index++) assertInputStep(steps[index]);
|
|
325
|
+
Object.defineProperty(protectedModel, 'last', {
|
|
326
|
+
value: withProviderTextBoundary(
|
|
327
|
+
steps[steps.length - 1] as ChatModel,
|
|
328
|
+
policy,
|
|
329
|
+
provider
|
|
330
|
+
),
|
|
331
|
+
});
|
|
332
|
+
return protectedModel;
|
|
333
|
+
}
|
|
334
|
+
if (!(model instanceof BaseChatModel) || model.cache != null) {
|
|
335
|
+
throw new ProviderTextProtectionError('unsupported');
|
|
336
|
+
}
|
|
337
|
+
if (model.transform !== BaseChatModel.prototype.transform ||
|
|
338
|
+
model._streamIterator !== BaseChatModel.prototype._streamIterator ||
|
|
339
|
+
model.generate !== BaseChatModel.prototype.generate ||
|
|
340
|
+
model.generatePrompt !== BaseChatModel.prototype.generatePrompt ||
|
|
341
|
+
model._generateUncached !== BaseChatModel.prototype._generateUncached) {
|
|
342
|
+
throw new ProviderTextProtectionError('unsupported');
|
|
343
|
+
}
|
|
344
|
+
if (model.stream !== BaseChatModel.prototype.stream || model.invoke !== BaseChatModel.prototype.invoke) {
|
|
345
|
+
const openAI = getChatModelClass(Providers.OPENAI);
|
|
346
|
+
if (model.stream !== openAI.prototype.stream || model.invoke !== openAI.prototype.invoke) {
|
|
347
|
+
throw new ProviderTextProtectionError('unsupported');
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
if (model.disableStreaming && usesInternalStreaming(model)) {
|
|
351
|
+
throw new ProviderTextProtectionError('unsupported');
|
|
352
|
+
}
|
|
353
|
+
const protectedChat = clone(model);
|
|
354
|
+
protectedChat._streamResponseChunks = async function* (
|
|
355
|
+
messages,
|
|
356
|
+
options,
|
|
357
|
+
runManager
|
|
358
|
+
): AsyncGenerator<ChatGenerationChunk> {
|
|
359
|
+
const attempt = new ProviderTextAttempt(policy, options.signal);
|
|
360
|
+
const candidate = new Candidate(attempt, provider);
|
|
361
|
+
let source: AsyncGenerator<ChatGenerationChunk> | undefined;
|
|
362
|
+
try {
|
|
363
|
+
source = model._streamResponseChunks(
|
|
364
|
+
messages,
|
|
365
|
+
{ ...options, signal: attempt.signal },
|
|
366
|
+
undefined
|
|
367
|
+
);
|
|
368
|
+
for (;;) {
|
|
369
|
+
const next = await attempt.wait(source.next());
|
|
370
|
+
attempt.check();
|
|
371
|
+
if (next.done === true) break;
|
|
372
|
+
const chunk = candidate.strip(next.value);
|
|
373
|
+
yield chunk;
|
|
374
|
+
await runManager?.handleLLMNewToken(
|
|
375
|
+
'',
|
|
376
|
+
undefined,
|
|
377
|
+
undefined,
|
|
378
|
+
undefined,
|
|
379
|
+
undefined,
|
|
380
|
+
{ chunk }
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
const canonical = await candidate.canonical();
|
|
384
|
+
attempt.check();
|
|
385
|
+
if (canonical != null) {
|
|
386
|
+
yield canonical;
|
|
387
|
+
attempt.check();
|
|
388
|
+
await runManager?.handleLLMNewToken(
|
|
389
|
+
canonical.text,
|
|
390
|
+
undefined,
|
|
391
|
+
undefined,
|
|
392
|
+
undefined,
|
|
393
|
+
undefined,
|
|
394
|
+
{ chunk: canonical }
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
} finally {
|
|
398
|
+
try {
|
|
399
|
+
if (source != null) void attempt.wait(source.return(undefined)).catch(() => {});
|
|
400
|
+
} finally {
|
|
401
|
+
attempt.finish();
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
};
|
|
405
|
+
protectedChat._streamChatModelEvents =
|
|
406
|
+
BaseChatModel.prototype._streamChatModelEvents;
|
|
407
|
+
protectedChat._generate = async function (messages, options): Promise<ChatResult> {
|
|
408
|
+
if (usesInternalStreaming(model, options)) throw new ProviderTextProtectionError('unsupported');
|
|
409
|
+
const attempt = new ProviderTextAttempt(policy, options.signal);
|
|
410
|
+
try {
|
|
411
|
+
const result = await attempt.wait(
|
|
412
|
+
model._generate(
|
|
413
|
+
messages,
|
|
414
|
+
{ ...options, signal: attempt.signal },
|
|
415
|
+
undefined
|
|
416
|
+
)
|
|
417
|
+
);
|
|
418
|
+
attempt.check();
|
|
419
|
+
if (result.llmOutput != null) validateMetadata(result.llmOutput, provider === Providers.GOOGLE);
|
|
420
|
+
if (result.generations.length !== 1)
|
|
421
|
+
throw new ProviderTextProtectionError('unsupported');
|
|
422
|
+
const generation = result.generations[0];
|
|
423
|
+
if (!(generation.message instanceof AIMessage)) throw new ProviderTextProtectionError('unsupported');
|
|
424
|
+
const message = generation.message instanceof AIMessageChunk ? generation.message : copyMessage(generation.message, generation.message.content);
|
|
425
|
+
const candidate = new Candidate(attempt, provider);
|
|
426
|
+
const stripped = candidate.strip(
|
|
427
|
+
new ChatGenerationChunk({ ...generation, message })
|
|
428
|
+
);
|
|
429
|
+
const canonical = await candidate.canonical();
|
|
430
|
+
attempt.check();
|
|
431
|
+
const released = canonical == null ? stripped : new ChatGenerationChunk({
|
|
432
|
+
text: canonical.text,
|
|
433
|
+
message: copyMessage(stripped.message as AIMessageChunk, mergeContent(stripped.message.content, canonical.message.content)),
|
|
434
|
+
generationInfo: stripped.generationInfo,
|
|
435
|
+
});
|
|
436
|
+
if (canonical != null) {
|
|
437
|
+
released.message.response_metadata = { ...released.message.response_metadata, ...canonical.message.response_metadata };
|
|
438
|
+
released.message.lc_kwargs.response_metadata = released.message.response_metadata;
|
|
439
|
+
}
|
|
440
|
+
return { ...result, generations: [released] };
|
|
441
|
+
} finally {
|
|
442
|
+
attempt.finish();
|
|
443
|
+
}
|
|
444
|
+
};
|
|
445
|
+
return protectedChat;
|
|
446
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { AIMessageChunk } from '@langchain/core/messages';
|
|
2
|
+
import type { AIMessageChunkFields } from '@langchain/core/messages';
|
|
3
|
+
|
|
4
|
+
type ChunkConstructor = new (fields: AIMessageChunkFields) => AIMessageChunk;
|
|
5
|
+
const constructors = new WeakMap<object, ChunkConstructor>();
|
|
6
|
+
|
|
7
|
+
/** Internal certification for SDK-owned aggregation semantics, not arbitrary provider subclasses. */
|
|
8
|
+
export function registerProviderTextChunkConstructor(constructor: ChunkConstructor): void {
|
|
9
|
+
constructors.set(constructor.prototype, constructor);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function createProviderTextChunk(source: object, fields: AIMessageChunkFields): AIMessageChunk {
|
|
13
|
+
const constructor = constructors.get(Object.getPrototypeOf(source)) ?? AIMessageChunk;
|
|
14
|
+
return new constructor(fields);
|
|
15
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import type { AIMessageChunk } from '@langchain/core/messages';
|
|
2
|
+
import type { ProviderTextAttempt } from '@/protection/providerText';
|
|
3
|
+
import { ProviderTextProtectionError } from '@/protection/providerText';
|
|
4
|
+
|
|
5
|
+
export const GEMINI_SIGNATURES = '__gemini_function_call_thought_signatures__';
|
|
6
|
+
export const RESPONSES_POSITIONS = '__openai_responses_replay_positions__';
|
|
7
|
+
|
|
8
|
+
export function keys(value: unknown, allowed: readonly string[]): value is Record<string, unknown> {
|
|
9
|
+
return value != null && typeof value === 'object' && !Array.isArray(value) &&
|
|
10
|
+
Object.keys(value).every((key) => allowed.includes(key));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function stringMap(value: unknown): boolean {
|
|
14
|
+
return value != null && typeof value === 'object' && !Array.isArray(value) &&
|
|
15
|
+
Object.entries(value).length <= 128 && Object.values(value).every((entry) => typeof entry === 'string');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function safetyRatings(value: unknown): boolean {
|
|
19
|
+
return Array.isArray(value) && value.length <= 16 && value.every((entry: unknown) =>
|
|
20
|
+
keys(entry, ['category', 'probability', 'blocked']) &&
|
|
21
|
+
typeof entry.category === 'string' && /^HARM_CATEGORY_[A-Z_]+$/.test(entry.category) &&
|
|
22
|
+
['NEGLIGIBLE', 'LOW', 'MEDIUM', 'HIGH', 'HARM_PROBABILITY_UNSPECIFIED'].includes(String(entry.probability)) &&
|
|
23
|
+
(entry.blocked == null || typeof entry.blocked === 'boolean'));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function replayPositions(value: unknown): boolean {
|
|
27
|
+
return Array.isArray(value) && value.length <= 128 && value.every((entry: unknown) =>
|
|
28
|
+
keys(entry, ['itemId', 'kind', 'outputIndex', 'contentIndex']) && typeof entry.itemId === 'string' &&
|
|
29
|
+
['message', 'text', 'reasoning', 'output'].includes(String(entry.kind)) &&
|
|
30
|
+
typeof entry.outputIndex === 'number' && Number.isSafeInteger(entry.outputIndex) && entry.outputIndex >= 0 &&
|
|
31
|
+
(entry.contentIndex == null || (typeof entry.contentIndex === 'number' && Number.isSafeInteger(entry.contentIndex) && entry.contentIndex >= 0)));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const scalarKeys = new Set([
|
|
35
|
+
'id', 'model', 'model_name', 'model_provider', 'created_at', 'object', 'status', 'user', 'service_tier',
|
|
36
|
+
'max_output_tokens', 'max_tool_calls', 'parallel_tool_calls', 'temperature', 'top_p', 'truncation',
|
|
37
|
+
'store', 'background', 'previous_response_id', 'prompt_cache_key', 'prompt_cache_retention',
|
|
38
|
+
'safety_identifier', 'completed_at',
|
|
39
|
+
]);
|
|
40
|
+
|
|
41
|
+
interface OutputText {
|
|
42
|
+
type: 'output_text';
|
|
43
|
+
text: string;
|
|
44
|
+
annotations: [];
|
|
45
|
+
logprobs?: [];
|
|
46
|
+
}
|
|
47
|
+
interface MessageOutput {
|
|
48
|
+
type: 'message';
|
|
49
|
+
id: string;
|
|
50
|
+
role: 'assistant';
|
|
51
|
+
status: 'completed' | 'incomplete';
|
|
52
|
+
phase?: 'commentary' | 'final_answer' | null;
|
|
53
|
+
content: [OutputText];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function messageOutput(value: unknown): value is MessageOutput {
|
|
57
|
+
if (!keys(value, ['id', 'type', 'role', 'status', 'phase', 'content']) || value.type !== 'message' ||
|
|
58
|
+
typeof value.id !== 'string' || value.role !== 'assistant' || !['completed', 'incomplete'].includes(String(value.status)) ||
|
|
59
|
+
(value.phase != null && !['commentary', 'final_answer'].includes(String(value.phase))) ||
|
|
60
|
+
!Array.isArray(value.content) || value.content.length !== 1) return false;
|
|
61
|
+
const part: unknown = value.content[0];
|
|
62
|
+
return keys(part, ['type', 'text', 'annotations', 'logprobs']) && part.type === 'output_text' &&
|
|
63
|
+
typeof part.text === 'string' && Array.isArray(part.annotations) && part.annotations.length === 0 &&
|
|
64
|
+
(part.logprobs === undefined || (Array.isArray(part.logprobs) && part.logprobs.length === 0));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** One certified prose output. Unknown structured outputs and alternate aliases stay gated. */
|
|
68
|
+
export class ResponsesTextProjection {
|
|
69
|
+
private metadata?: AIMessageChunk['response_metadata'];
|
|
70
|
+
private hasOutputAlias = false;
|
|
71
|
+
private output?: Array<MessageOutput | Record<string, unknown>>;
|
|
72
|
+
|
|
73
|
+
constructor(private readonly attempt: ProviderTextAttempt) {}
|
|
74
|
+
|
|
75
|
+
strip(metadata: AIMessageChunk['response_metadata']): AIMessageChunk['response_metadata'] {
|
|
76
|
+
if (!Object.hasOwn(metadata, 'output')) return metadata;
|
|
77
|
+
if (this.metadata != null || metadata.model_provider !== 'openai' || metadata.object !== 'response' ||
|
|
78
|
+
!Array.isArray(metadata.output) || metadata.output.length > 32) {
|
|
79
|
+
throw new ProviderTextProtectionError('unsupported');
|
|
80
|
+
}
|
|
81
|
+
let messageCount = 0;
|
|
82
|
+
const output: Array<MessageOutput | Record<string, unknown>> = metadata.output.map((item: unknown) => {
|
|
83
|
+
if (messageOutput(item)) {
|
|
84
|
+
if (++messageCount > 1 || !this.attempt.matchesText(item.content[0].text)) throw new ProviderTextProtectionError('unsupported');
|
|
85
|
+
return { ...item, content: [{ ...item.content[0], text: '' }] };
|
|
86
|
+
}
|
|
87
|
+
if (!responseOutputControl(item)) throw new ProviderTextProtectionError('unsupported');
|
|
88
|
+
return item;
|
|
89
|
+
});
|
|
90
|
+
if ((messageCount === 0 && !this.attempt.matchesText('')) ||
|
|
91
|
+
(metadata.output_text != null && (typeof metadata.output_text !== 'string' || !this.attempt.matchesText(metadata.output_text)))) {
|
|
92
|
+
throw new ProviderTextProtectionError('unsupported');
|
|
93
|
+
}
|
|
94
|
+
for (const [key, value] of Object.entries(metadata)) {
|
|
95
|
+
let valid: boolean;
|
|
96
|
+
if (key === 'output' || key === 'output_text') continue;
|
|
97
|
+
if (key === 'top_logprobs') valid = value == null || value === 0;
|
|
98
|
+
else if (key === 'usage') valid = numericTree(value);
|
|
99
|
+
else if (key === 'text') valid = keys(value, ['format', 'verbosity']) && keys(value.format, ['type']) && value.format.type === 'text' &&
|
|
100
|
+
(value.verbosity == null || ['low', 'medium', 'high'].includes(String(value.verbosity)));
|
|
101
|
+
else if (key === 'tools') valid = Array.isArray(value) && value.length <= 32 && value.every(responseTool);
|
|
102
|
+
else if (key === 'tool_choice') valid = value === 'auto' || value === 'none' || value === 'required' || (keys(value, ['type', 'name']) && value.type === 'function' && typeof value.name === 'string');
|
|
103
|
+
else if (key === 'reasoning') valid = value == null || (keys(value, ['effort', 'summary']) &&
|
|
104
|
+
(value.effort == null || ['none', 'minimal', 'low', 'medium', 'high', 'xhigh'].includes(String(value.effort))) &&
|
|
105
|
+
(value.summary == null || ['auto', 'concise', 'detailed'].includes(String(value.summary))));
|
|
106
|
+
else if (key === 'metadata') valid = keys(value, []) || value == null;
|
|
107
|
+
else if (key === 'instructions' || key === 'error') valid = value == null;
|
|
108
|
+
else if (key === 'incomplete_details') valid = value == null || (keys(value, ['reason']) && ['max_output_tokens', 'content_filter'].includes(String(value.reason)));
|
|
109
|
+
else valid = scalarKeys.has(key) && (value == null || typeof value === 'string' || typeof value === 'boolean' || (typeof value === 'number' && Number.isFinite(value)));
|
|
110
|
+
if (!valid) throw new ProviderTextProtectionError('unsupported');
|
|
111
|
+
}
|
|
112
|
+
retainTree(metadata, this.attempt);
|
|
113
|
+
this.output = output;
|
|
114
|
+
this.hasOutputAlias = Object.hasOwn(metadata, 'output_text');
|
|
115
|
+
const { output: _output, output_text: _alias, ...controls } = metadata;
|
|
116
|
+
this.metadata = controls;
|
|
117
|
+
return controls;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
canonical(content: string): AIMessageChunk['response_metadata'] | undefined {
|
|
121
|
+
if (this.metadata == null || this.output == null) return undefined;
|
|
122
|
+
return {
|
|
123
|
+
output: this.output.map((item) => messageOutput(item) ? { ...item, content: [{ ...item.content[0], text: content }] } : item),
|
|
124
|
+
...(this.hasOutputAlias ? { output_text: content } : {}),
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function numericTree(value: unknown, depth = 4): boolean {
|
|
130
|
+
if (value == null) return true;
|
|
131
|
+
if (typeof value === 'number') return Number.isFinite(value);
|
|
132
|
+
if (typeof value !== 'object' || Array.isArray(value) || depth === 0) return false;
|
|
133
|
+
const entries = Object.values(value);
|
|
134
|
+
return entries.length <= 64 && entries.every((entry) => numericTree(entry, depth - 1));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function responseOutputControl(value: unknown): value is Record<string, unknown> {
|
|
138
|
+
if (!keys(value, ['id', 'type', 'status', 'call_id', 'name', 'arguments', 'summary', 'encrypted_content'])) return false;
|
|
139
|
+
if (typeof value.id !== 'string' || (value.status != null && !['completed', 'incomplete', 'in_progress'].includes(String(value.status)))) return false;
|
|
140
|
+
if (value.type === 'function_call') return keys(value, ['id', 'type', 'status', 'call_id', 'name', 'arguments']) &&
|
|
141
|
+
typeof value.call_id === 'string' && typeof value.name === 'string' && typeof value.arguments === 'string';
|
|
142
|
+
if (value.type !== 'reasoning' || !keys(value, ['id', 'type', 'status', 'summary', 'encrypted_content'])) return false;
|
|
143
|
+
return (value.encrypted_content == null || typeof value.encrypted_content === 'string') &&
|
|
144
|
+
Array.isArray(value.summary) && value.summary.length <= 32 && value.summary.every((entry: unknown) =>
|
|
145
|
+
keys(entry, ['type', 'text']) && entry.type === 'summary_text' && typeof entry.text === 'string');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function responseTool(value: unknown): boolean {
|
|
149
|
+
return keys(value, ['type', 'name', 'description', 'strict', 'parameters']) && value.type === 'function' &&
|
|
150
|
+
typeof value.name === 'string' && (value.description == null || typeof value.description === 'string') &&
|
|
151
|
+
(value.strict == null || typeof value.strict === 'boolean') && jsonControl(value.parameters);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function jsonControl(value: unknown, depth = 6): boolean {
|
|
155
|
+
if (value == null || typeof value === 'string' || typeof value === 'boolean') return true;
|
|
156
|
+
if (typeof value === 'number') return Number.isFinite(value);
|
|
157
|
+
if (typeof value !== 'object' || depth === 0) return false;
|
|
158
|
+
const entries = Object.values(value);
|
|
159
|
+
return entries.length <= 64 && entries.every((entry) => jsonControl(entry, depth - 1));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function retainTree(value: unknown, attempt: ProviderTextAttempt, depth = 8): void {
|
|
163
|
+
if (typeof value === 'string') { attempt.retain(value.length * 2 + 32); return; }
|
|
164
|
+
attempt.retain(64);
|
|
165
|
+
if (value == null || typeof value !== 'object') return;
|
|
166
|
+
if (depth === 0) throw new ProviderTextProtectionError('unsupported');
|
|
167
|
+
const entries = Object.entries(value);
|
|
168
|
+
if (entries.length > 128) throw new ProviderTextProtectionError('unsupported');
|
|
169
|
+
for (const [key, entry] of entries) {
|
|
170
|
+
attempt.retain(key.length * 2 + 32);
|
|
171
|
+
retainTree(entry, attempt, depth - 1);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { isProtectedProviderTextSignal } from '@/protection/providerText';
|
|
2
|
+
|
|
1
3
|
export const DEFAULT_STREAM_DELAY = 25;
|
|
2
4
|
export const SMOOTH_TARGET_LATENCY_MS = 250;
|
|
3
5
|
export const MAX_STREAM_QUEUE_CHUNKS = 256;
|
|
@@ -204,8 +206,8 @@ export async function* smoothStream<TEmit>({
|
|
|
204
206
|
signal?: AbortSignal;
|
|
205
207
|
abortUpstream?: () => void;
|
|
206
208
|
}): AsyncGenerator<TEmit> {
|
|
207
|
-
if (!(delayMs > 0)) {
|
|
208
|
-
/** Disabled smoothing preserves fully lazy streaming: no background
|
|
209
|
+
if (!(delayMs > 0) || isProtectedProviderTextSignal(signal)) {
|
|
210
|
+
/** Disabled/protected smoothing preserves fully lazy streaming: no background
|
|
209
211
|
* producer, no read-ahead — each provider chunk is pulled only when the
|
|
210
212
|
* consumer asks, exactly like the pre-engine pass-through paths. */
|
|
211
213
|
for await (const item of source) {
|