@jsonstudio/llms 0.4.6 → 0.6.2
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/conversion/codecs/anthropic-openai-codec.js +28 -2
- package/dist/conversion/codecs/gemini-openai-codec.js +23 -0
- package/dist/conversion/codecs/responses-openai-codec.js +8 -1
- package/dist/conversion/hub/node-support.js +14 -1
- package/dist/conversion/hub/pipeline/hub-pipeline.d.ts +66 -0
- package/dist/conversion/hub/pipeline/hub-pipeline.js +284 -193
- package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage1_format_parse/index.d.ts +11 -0
- package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage1_format_parse/index.js +6 -0
- package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage2_semantic_map/index.d.ts +16 -0
- package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage2_semantic_map/index.js +17 -0
- package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage3_context_capture/context-factories.d.ts +5 -0
- package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage3_context_capture/context-factories.js +17 -0
- package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage3_context_capture/index.d.ts +19 -0
- package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage3_context_capture/index.js +269 -0
- package/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage1_semantic_map/index.d.ts +18 -0
- package/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage1_semantic_map/index.js +141 -0
- package/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage2_format_build/index.d.ts +11 -0
- package/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage2_format_build/index.js +29 -0
- package/dist/conversion/hub/pipeline/stages/req_process/req_process_stage1_tool_governance/index.d.ts +16 -0
- package/dist/conversion/hub/pipeline/stages/req_process/req_process_stage1_tool_governance/index.js +15 -0
- package/dist/conversion/hub/pipeline/stages/req_process/req_process_stage2_route_select/index.d.ts +17 -0
- package/dist/conversion/hub/pipeline/stages/req_process/req_process_stage2_route_select/index.js +18 -0
- package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage1_sse_decode/index.d.ts +17 -0
- package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage1_sse_decode/index.js +63 -0
- package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage2_format_parse/index.d.ts +11 -0
- package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage2_format_parse/index.js +6 -0
- package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage3_semantic_map/index.d.ts +12 -0
- package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage3_semantic_map/index.js +6 -0
- package/dist/conversion/hub/pipeline/stages/resp_outbound/resp_outbound_stage1_client_remap/index.d.ts +13 -0
- package/dist/conversion/hub/pipeline/stages/resp_outbound/resp_outbound_stage1_client_remap/index.js +43 -0
- package/dist/conversion/hub/pipeline/stages/resp_outbound/resp_outbound_stage2_sse_stream/index.d.ts +17 -0
- package/dist/conversion/hub/pipeline/stages/resp_outbound/resp_outbound_stage2_sse_stream/index.js +22 -0
- package/dist/conversion/hub/pipeline/stages/resp_process/resp_process_stage1_tool_governance/index.d.ts +16 -0
- package/dist/conversion/hub/pipeline/stages/resp_process/resp_process_stage1_tool_governance/index.js +19 -0
- package/dist/conversion/hub/pipeline/stages/resp_process/resp_process_stage2_finalize/index.d.ts +17 -0
- package/dist/conversion/hub/pipeline/stages/resp_process/resp_process_stage2_finalize/index.js +19 -0
- package/dist/conversion/hub/pipeline/stages/utils.d.ts +2 -0
- package/dist/conversion/hub/pipeline/stages/utils.js +11 -0
- package/dist/conversion/hub/pipeline/target-utils.d.ts +5 -0
- package/dist/conversion/hub/pipeline/target-utils.js +87 -0
- package/dist/conversion/hub/process/chat-process.js +23 -17
- package/dist/conversion/hub/response/provider-response.js +69 -122
- package/dist/conversion/hub/response/response-mappers.d.ts +19 -0
- package/dist/conversion/hub/response/response-mappers.js +22 -2
- package/dist/conversion/hub/response/response-runtime.d.ts +8 -0
- package/dist/conversion/hub/response/response-runtime.js +239 -6
- package/dist/conversion/hub/semantic-mappers/anthropic-mapper.d.ts +8 -0
- package/dist/conversion/hub/semantic-mappers/anthropic-mapper.js +135 -55
- package/dist/conversion/hub/semantic-mappers/chat-mapper.js +80 -40
- package/dist/conversion/hub/semantic-mappers/gemini-mapper.js +5 -29
- package/dist/conversion/hub/semantic-mappers/responses-mapper.js +16 -13
- package/dist/conversion/hub/snapshot-recorder.d.ts +13 -0
- package/dist/conversion/hub/snapshot-recorder.js +90 -50
- package/dist/conversion/hub/standardized-bridge.js +49 -38
- package/dist/conversion/hub/types/chat-envelope.d.ts +68 -0
- package/dist/conversion/hub/types/standardized.d.ts +97 -0
- package/dist/conversion/pipeline/codecs/v2/anthropic-openai-pipeline.js +29 -2
- package/dist/conversion/pipeline/codecs/v2/responses-openai-pipeline.js +68 -1
- package/dist/conversion/responses/responses-openai-bridge.d.ts +6 -1
- package/dist/conversion/responses/responses-openai-bridge.js +132 -10
- package/dist/conversion/shared/anthropic-message-utils.d.ts +9 -1
- package/dist/conversion/shared/anthropic-message-utils.js +414 -26
- package/dist/conversion/shared/bridge-actions.js +267 -95
- package/dist/conversion/shared/bridge-message-utils.js +54 -8
- package/dist/conversion/shared/bridge-policies.js +21 -2
- package/dist/conversion/shared/chat-envelope-validator.d.ts +8 -0
- package/dist/conversion/shared/chat-envelope-validator.js +128 -0
- package/dist/conversion/shared/chat-request-filters.js +109 -28
- package/dist/conversion/shared/mcp-injection.js +41 -20
- package/dist/conversion/shared/openai-finalizer.d.ts +11 -0
- package/dist/conversion/shared/openai-finalizer.js +73 -0
- package/dist/conversion/shared/openai-message-normalize.js +32 -31
- package/dist/conversion/shared/protocol-state.d.ts +4 -0
- package/dist/conversion/shared/protocol-state.js +23 -0
- package/dist/conversion/shared/reasoning-normalizer.d.ts +1 -0
- package/dist/conversion/shared/reasoning-normalizer.js +50 -18
- package/dist/conversion/shared/responses-output-builder.d.ts +1 -1
- package/dist/conversion/shared/responses-output-builder.js +76 -25
- package/dist/conversion/shared/responses-reasoning-registry.d.ts +8 -0
- package/dist/conversion/shared/responses-reasoning-registry.js +61 -0
- package/dist/conversion/shared/responses-response-utils.js +32 -2
- package/dist/conversion/shared/responses-tool-utils.js +28 -2
- package/dist/conversion/shared/snapshot-hooks.d.ts +9 -0
- package/dist/conversion/shared/snapshot-hooks.js +60 -6
- package/dist/conversion/shared/snapshot-utils.d.ts +16 -0
- package/dist/conversion/shared/snapshot-utils.js +84 -0
- package/dist/conversion/shared/tool-filter-pipeline.js +46 -7
- package/dist/conversion/shared/tool-mapping.js +13 -2
- package/dist/filters/index.d.ts +18 -0
- package/dist/filters/index.js +0 -1
- package/dist/filters/special/request-streaming-to-nonstreaming.d.ts +13 -0
- package/dist/filters/special/request-streaming-to-nonstreaming.js +13 -1
- package/dist/filters/special/request-tool-choice-policy.js +3 -1
- package/dist/filters/special/request-tool-list-filter.d.ts +11 -0
- package/dist/filters/special/request-tool-list-filter.js +20 -7
- package/dist/sse/shared/responses-output-normalizer.js +5 -4
- package/dist/sse/sse-to-json/builders/response-builder.js +24 -1
- package/dist/sse/types/responses-types.d.ts +2 -0
- package/package.json +1 -1
|
@@ -6,26 +6,58 @@ function isRecord(value) {
|
|
|
6
6
|
return Boolean(value && typeof value === 'object' && !Array.isArray(value));
|
|
7
7
|
}
|
|
8
8
|
export function normalizeReasoningInChatPayload(payload) {
|
|
9
|
-
if (!payload
|
|
9
|
+
if (!payload) {
|
|
10
10
|
return;
|
|
11
11
|
}
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
12
|
+
if (Array.isArray(payload.messages)) {
|
|
13
|
+
payload.messages = payload.messages.map(entry => normalizeChatMessageEntry(entry));
|
|
14
|
+
}
|
|
15
|
+
if (Array.isArray(payload.choices)) {
|
|
16
|
+
payload.choices.forEach(choice => {
|
|
17
|
+
normalizeChatChoice(choice);
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function normalizeChatMessageEntry(entry) {
|
|
22
|
+
if (!entry || typeof entry !== 'object') {
|
|
23
|
+
return entry;
|
|
24
|
+
}
|
|
25
|
+
const msg = entry;
|
|
26
|
+
applyNormalizedChatContent(msg);
|
|
27
|
+
return msg;
|
|
28
|
+
}
|
|
29
|
+
function normalizeChatChoice(choice) {
|
|
30
|
+
if (!choice || typeof choice !== 'object') {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const record = choice;
|
|
34
|
+
const containers = [];
|
|
35
|
+
if (record.message && typeof record.message === 'object') {
|
|
36
|
+
containers.push(record.message);
|
|
37
|
+
}
|
|
38
|
+
if (record.delta && typeof record.delta === 'object') {
|
|
39
|
+
containers.push(record.delta);
|
|
40
|
+
}
|
|
41
|
+
containers.forEach(container => applyNormalizedChatContent(container));
|
|
42
|
+
}
|
|
43
|
+
function applyNormalizedChatContent(container) {
|
|
44
|
+
const normalized = normalizeChatMessageContent(container.content);
|
|
45
|
+
const role = typeof container.role === 'string' ? container.role : undefined;
|
|
46
|
+
if (normalized.contentText !== undefined && normalized.contentText.trim().length) {
|
|
47
|
+
container.content = normalized.contentText;
|
|
48
|
+
}
|
|
49
|
+
else if (typeof container.reasoning_content === 'string' && container.reasoning_content.trim().length) {
|
|
50
|
+
container.content = container.reasoning_content.trim();
|
|
51
|
+
}
|
|
52
|
+
else if (role && role !== 'system' && role !== 'tool') {
|
|
53
|
+
container.content = '';
|
|
54
|
+
}
|
|
55
|
+
if (normalized.reasoningText && normalized.reasoningText.trim().length) {
|
|
56
|
+
container.reasoning_content = normalized.reasoningText.trim();
|
|
57
|
+
}
|
|
58
|
+
else if ('reasoning_content' in container) {
|
|
59
|
+
delete container.reasoning_content;
|
|
60
|
+
}
|
|
29
61
|
}
|
|
30
62
|
export function normalizeReasoningInResponsesPayload(payload, options = { includeOutput: true }) {
|
|
31
63
|
if (!payload) {
|
|
@@ -7,7 +7,7 @@ export interface BuildResponsesOutputOptions {
|
|
|
7
7
|
}
|
|
8
8
|
export interface BuildResponsesOutputResult {
|
|
9
9
|
outputItems: ResponsesOutputItem[];
|
|
10
|
-
outputText
|
|
10
|
+
outputText?: string;
|
|
11
11
|
status: string;
|
|
12
12
|
requiredAction?: Record<string, unknown>;
|
|
13
13
|
usage?: unknown;
|
|
@@ -1,17 +1,64 @@
|
|
|
1
1
|
import { normalizeFunctionCallId } from './bridge-id-utils.js';
|
|
2
2
|
import { normalizeContentPart } from './output-content-normalizer.js';
|
|
3
3
|
import { expandResponsesMessageItem } from '../../sse/shared/responses-output-normalizer.js';
|
|
4
|
+
function appendReasoningSegments(target, raw) {
|
|
5
|
+
if (typeof raw !== 'string' || !raw.length) {
|
|
6
|
+
return;
|
|
7
|
+
}
|
|
8
|
+
const segments = raw.split('\n');
|
|
9
|
+
let previousValue;
|
|
10
|
+
for (const segment of segments) {
|
|
11
|
+
if (!segment.length)
|
|
12
|
+
continue;
|
|
13
|
+
let value = segment;
|
|
14
|
+
if (previousValue) {
|
|
15
|
+
const prevLast = previousValue.charAt(previousValue.length - 1) || '';
|
|
16
|
+
const currFirst = segment.charAt(0) || '';
|
|
17
|
+
const prevWord = /[A-Za-z0-9_]$/.test(prevLast);
|
|
18
|
+
const currWord = /^[A-Za-z0-9]/.test(currFirst);
|
|
19
|
+
const currQuote = currFirst === '"' || currFirst === "'";
|
|
20
|
+
const prevPunct = /[:;]/.test(prevLast);
|
|
21
|
+
const prevEndsWhitespace = /\s$/.test(previousValue);
|
|
22
|
+
const prevSentenceEnd = /[.?!"]$/.test(prevLast);
|
|
23
|
+
if (((prevWord || prevSentenceEnd) && currWord && !prevEndsWhitespace) ||
|
|
24
|
+
(prevPunct && currQuote)) {
|
|
25
|
+
value = ` ${segment}`;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
target.push(value);
|
|
29
|
+
previousValue = value;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
4
32
|
export function buildResponsesOutputFromChat(options) {
|
|
5
33
|
const { response, message, requestId, sanitizeFunctionName } = options;
|
|
6
34
|
const outputItems = [];
|
|
7
35
|
const allocateOutputId = (prefix) => `${prefix}_${requestId ?? Date.now()}_${outputItems.length + 1}`;
|
|
8
36
|
const role = message?.role || 'assistant';
|
|
9
37
|
const content = message?.content;
|
|
10
|
-
|
|
11
|
-
|
|
38
|
+
let toolCalls = Array.isArray(message?.tool_calls) ? message.tool_calls : [];
|
|
39
|
+
try {
|
|
40
|
+
toolCalls = toolCalls.filter((it) => {
|
|
41
|
+
const nm = (it && typeof it === 'object') ? (it?.function?.name || it.name) : undefined;
|
|
42
|
+
return typeof nm === 'string' && nm.trim().length > 0 && nm.toLowerCase() !== 'tool';
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
/* ignore */
|
|
47
|
+
}
|
|
48
|
+
const hasToolCalls = toolCalls.length > 0;
|
|
49
|
+
const reasoningChunks = [];
|
|
50
|
+
const preservedReasoning = Array.isArray(response?.__responses_reasoning)
|
|
51
|
+
? response.__responses_reasoning
|
|
12
52
|
: undefined;
|
|
53
|
+
if (preservedReasoning && preservedReasoning.length) {
|
|
54
|
+
reasoningChunks.push(...preservedReasoning);
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
appendReasoningSegments(reasoningChunks, message?.reasoning_content);
|
|
58
|
+
}
|
|
13
59
|
const convertedContent = convertChatContentToResponses(content);
|
|
14
|
-
|
|
60
|
+
const shouldEmitMessage = Boolean(message) && (convertedContent.length > 0 || reasoningChunks.length > 0 || !hasToolCalls);
|
|
61
|
+
if (shouldEmitMessage) {
|
|
15
62
|
const responsesMessage = {
|
|
16
63
|
id: allocateOutputId('message'),
|
|
17
64
|
type: 'message',
|
|
@@ -22,23 +69,13 @@ export function buildResponsesOutputFromChat(options) {
|
|
|
22
69
|
const expandedItems = expandResponsesMessageItem(responsesMessage, {
|
|
23
70
|
requestId: requestId ?? 'responses_outbound',
|
|
24
71
|
outputIndex: outputItems.length,
|
|
25
|
-
extraReasoning:
|
|
72
|
+
extraReasoning: reasoningChunks
|
|
26
73
|
});
|
|
27
74
|
for (const expanded of expandedItems) {
|
|
28
75
|
outputItems.push(expanded);
|
|
29
76
|
}
|
|
30
77
|
}
|
|
31
|
-
let toolCalls = Array.isArray(message?.tool_calls) ? message.tool_calls : [];
|
|
32
78
|
const normalizedToolCalls = [];
|
|
33
|
-
try {
|
|
34
|
-
toolCalls = toolCalls.filter((it) => {
|
|
35
|
-
const nm = (it && typeof it === 'object') ? (it?.function?.name || it.name) : undefined;
|
|
36
|
-
return typeof nm === 'string' && nm.trim().length > 0 && nm.toLowerCase() !== 'tool';
|
|
37
|
-
});
|
|
38
|
-
}
|
|
39
|
-
catch {
|
|
40
|
-
/* ignore */
|
|
41
|
-
}
|
|
42
79
|
let toolFallbackCounter = 0;
|
|
43
80
|
for (const call of toolCalls) {
|
|
44
81
|
const outputEntry = buildFunctionCallOutput(call, allocateOutputId, sanitizeFunctionName, outputItems.length, ++toolFallbackCounter);
|
|
@@ -52,19 +89,20 @@ export function buildResponsesOutputFromChat(options) {
|
|
|
52
89
|
}
|
|
53
90
|
}
|
|
54
91
|
const usage = normalizeUsage(response.usage);
|
|
55
|
-
const
|
|
56
|
-
const
|
|
57
|
-
|
|
92
|
+
const outputTextMeta = response?.__responses_output_text_meta;
|
|
93
|
+
const outputText = resolveOutputText(convertedContent, outputTextMeta);
|
|
94
|
+
const hasNormalizedToolCalls = normalizedToolCalls.length > 0;
|
|
95
|
+
if (hasNormalizedToolCalls) {
|
|
58
96
|
for (const item of outputItems) {
|
|
59
97
|
if (item.type === 'message') {
|
|
60
98
|
item.status = 'in_progress';
|
|
61
99
|
}
|
|
62
100
|
}
|
|
63
101
|
}
|
|
64
|
-
const requiredAction =
|
|
102
|
+
const requiredAction = hasNormalizedToolCalls
|
|
65
103
|
? buildRequiredActionFromNormalized(normalizedToolCalls)
|
|
66
104
|
: undefined;
|
|
67
|
-
const status =
|
|
105
|
+
const status = hasNormalizedToolCalls ? 'requires_action' : 'completed';
|
|
68
106
|
return {
|
|
69
107
|
outputItems,
|
|
70
108
|
outputText,
|
|
@@ -121,12 +159,10 @@ function buildFunctionCallOutput(call, allocateOutputId, sanitizeFunctionName, b
|
|
|
121
159
|
const originalCallId = typeof call.id === 'string' && call.id.trim().length
|
|
122
160
|
? String(call.id)
|
|
123
161
|
: (typeof call.call_id === 'string' && call.call_id.trim().length ? String(call.call_id) : undefined);
|
|
124
|
-
const callId =
|
|
125
|
-
|
|
126
|
-
:
|
|
127
|
-
|
|
128
|
-
fallback: `fc_call_${baseCount + offset}`
|
|
129
|
-
});
|
|
162
|
+
const callId = normalizeFunctionCallId({
|
|
163
|
+
callId: originalCallId,
|
|
164
|
+
fallback: `fc_call_${baseCount + offset}`
|
|
165
|
+
});
|
|
130
166
|
const output = {
|
|
131
167
|
id: allocateOutputId('function_call'),
|
|
132
168
|
type: 'function_call',
|
|
@@ -177,3 +213,18 @@ function extractOutputText(parts) {
|
|
|
177
213
|
const text = normalizedTexts.join('\n').trim();
|
|
178
214
|
return text.length ? text : '';
|
|
179
215
|
}
|
|
216
|
+
function resolveOutputText(parts, meta) {
|
|
217
|
+
if (meta && typeof meta === 'object') {
|
|
218
|
+
const hasField = Boolean(meta.hasField);
|
|
219
|
+
if (hasField) {
|
|
220
|
+
const raw = meta.value;
|
|
221
|
+
if (typeof raw === 'string') {
|
|
222
|
+
return raw;
|
|
223
|
+
}
|
|
224
|
+
return '';
|
|
225
|
+
}
|
|
226
|
+
return undefined;
|
|
227
|
+
}
|
|
228
|
+
const text = extractOutputText(parts);
|
|
229
|
+
return text.length ? text : undefined;
|
|
230
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface ResponsesOutputTextMeta {
|
|
2
|
+
hasField: boolean;
|
|
3
|
+
value?: string;
|
|
4
|
+
}
|
|
5
|
+
export declare function registerResponsesReasoning(id: unknown, segments: string[] | undefined): void;
|
|
6
|
+
export declare function consumeResponsesReasoning(id: unknown): string[] | undefined;
|
|
7
|
+
export declare function registerResponsesOutputTextMeta(id: unknown, meta: ResponsesOutputTextMeta | undefined): void;
|
|
8
|
+
export declare function consumeResponsesOutputTextMeta(id: unknown): ResponsesOutputTextMeta | undefined;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
const registry = new Map();
|
|
2
|
+
function ensureEntry(id) {
|
|
3
|
+
let entry = registry.get(id);
|
|
4
|
+
if (!entry) {
|
|
5
|
+
entry = {};
|
|
6
|
+
registry.set(id, entry);
|
|
7
|
+
}
|
|
8
|
+
return entry;
|
|
9
|
+
}
|
|
10
|
+
function pruneEntry(id) {
|
|
11
|
+
const entry = registry.get(id);
|
|
12
|
+
if (!entry)
|
|
13
|
+
return;
|
|
14
|
+
if (!entry.reasoning && !entry.outputText) {
|
|
15
|
+
registry.delete(id);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export function registerResponsesReasoning(id, segments) {
|
|
19
|
+
if (typeof id !== 'string')
|
|
20
|
+
return;
|
|
21
|
+
if (!Array.isArray(segments) || segments.length === 0)
|
|
22
|
+
return;
|
|
23
|
+
const entry = ensureEntry(id);
|
|
24
|
+
entry.reasoning = [...segments];
|
|
25
|
+
}
|
|
26
|
+
export function consumeResponsesReasoning(id) {
|
|
27
|
+
if (typeof id !== 'string')
|
|
28
|
+
return undefined;
|
|
29
|
+
const entry = registry.get(id);
|
|
30
|
+
if (!entry?.reasoning)
|
|
31
|
+
return undefined;
|
|
32
|
+
const value = [...entry.reasoning];
|
|
33
|
+
entry.reasoning = undefined;
|
|
34
|
+
pruneEntry(id);
|
|
35
|
+
return value;
|
|
36
|
+
}
|
|
37
|
+
export function registerResponsesOutputTextMeta(id, meta) {
|
|
38
|
+
if (typeof id !== 'string')
|
|
39
|
+
return;
|
|
40
|
+
if (!meta)
|
|
41
|
+
return;
|
|
42
|
+
const entry = ensureEntry(id);
|
|
43
|
+
entry.outputText = {
|
|
44
|
+
hasField: Boolean(meta.hasField),
|
|
45
|
+
value: typeof meta.value === 'string' ? meta.value : undefined
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
export function consumeResponsesOutputTextMeta(id) {
|
|
49
|
+
if (typeof id !== 'string')
|
|
50
|
+
return undefined;
|
|
51
|
+
const entry = registry.get(id);
|
|
52
|
+
if (!entry?.outputText)
|
|
53
|
+
return undefined;
|
|
54
|
+
const value = {
|
|
55
|
+
hasField: Boolean(entry.outputText.hasField),
|
|
56
|
+
value: entry.outputText.value
|
|
57
|
+
};
|
|
58
|
+
entry.outputText = undefined;
|
|
59
|
+
pruneEntry(id);
|
|
60
|
+
return value;
|
|
61
|
+
}
|
|
@@ -135,6 +135,24 @@ function unwrapResponsesResponse(payload) {
|
|
|
135
135
|
}
|
|
136
136
|
return undefined;
|
|
137
137
|
}
|
|
138
|
+
function collectRawReasoningSegments(response) {
|
|
139
|
+
const segments = [];
|
|
140
|
+
const outputItems = Array.isArray(response.output) ? response.output : [];
|
|
141
|
+
for (const item of outputItems) {
|
|
142
|
+
if (!item || typeof item !== 'object')
|
|
143
|
+
continue;
|
|
144
|
+
const type = typeof item.type === 'string' ? String(item.type).toLowerCase() : '';
|
|
145
|
+
if (type !== 'reasoning')
|
|
146
|
+
continue;
|
|
147
|
+
const content = Array.isArray(item.content) ? item.content : [];
|
|
148
|
+
for (const part of content) {
|
|
149
|
+
if (part && typeof part === 'object' && typeof part.text === 'string') {
|
|
150
|
+
segments.push(part.text);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return segments;
|
|
155
|
+
}
|
|
138
156
|
export function buildChatResponseFromResponses(payload) {
|
|
139
157
|
if (!payload || typeof payload !== 'object')
|
|
140
158
|
return payload;
|
|
@@ -152,6 +170,7 @@ export function buildChatResponseFromResponses(payload) {
|
|
|
152
170
|
const usage = response.usage;
|
|
153
171
|
const toolCalls = collectToolCallsFromResponses(response);
|
|
154
172
|
const { textParts, reasoningParts } = extractOutputSegments(response);
|
|
173
|
+
const rawReasoningSegments = collectRawReasoningSegments(response);
|
|
155
174
|
const explicitOutput = typeof response.output_text === 'string' && response.output_text.trim().length
|
|
156
175
|
? sanitizeReasoningTaggedText(response.output_text)
|
|
157
176
|
: undefined;
|
|
@@ -185,8 +204,9 @@ export function buildChatResponseFromResponses(payload) {
|
|
|
185
204
|
if (toolCalls.length) {
|
|
186
205
|
message.tool_calls = toolCalls;
|
|
187
206
|
}
|
|
188
|
-
|
|
189
|
-
|
|
207
|
+
const reasoningSegments = rawReasoningSegments.length ? rawReasoningSegments : reasoningParts;
|
|
208
|
+
if (reasoningSegments.length) {
|
|
209
|
+
message.reasoning_content = reasoningSegments.join('\n');
|
|
190
210
|
}
|
|
191
211
|
const finishReason = resolveFinishReason(response, toolCalls);
|
|
192
212
|
const chat = {
|
|
@@ -202,6 +222,16 @@ export function buildChatResponseFromResponses(payload) {
|
|
|
202
222
|
}
|
|
203
223
|
]
|
|
204
224
|
};
|
|
225
|
+
const hasOutputTextField = Object.prototype.hasOwnProperty.call(response, 'output_text');
|
|
226
|
+
if (rawReasoningSegments.length) {
|
|
227
|
+
chat.__responses_reasoning = rawReasoningSegments;
|
|
228
|
+
}
|
|
229
|
+
chat.__responses_output_text_meta = {
|
|
230
|
+
hasField: hasOutputTextField,
|
|
231
|
+
value: hasOutputTextField && typeof response.output_text === 'string'
|
|
232
|
+
? sanitizeReasoningTaggedText(response.output_text)
|
|
233
|
+
: undefined
|
|
234
|
+
};
|
|
205
235
|
if (usage !== undefined) {
|
|
206
236
|
chat.usage = usage;
|
|
207
237
|
}
|
|
@@ -68,10 +68,36 @@ export function resolveToolCallIdStyle(metadata) {
|
|
|
68
68
|
export function stripInternalToolingMetadata(metadata) {
|
|
69
69
|
if (!metadata || typeof metadata !== 'object')
|
|
70
70
|
return;
|
|
71
|
-
|
|
72
|
-
|
|
71
|
+
const record = metadata;
|
|
72
|
+
if ('toolCallIdStyle' in record) {
|
|
73
|
+
delete record.toolCallIdStyle;
|
|
74
|
+
}
|
|
75
|
+
if (RAW_SYSTEM_SENTINEL in record) {
|
|
76
|
+
delete record[RAW_SYSTEM_SENTINEL];
|
|
77
|
+
}
|
|
78
|
+
if (record.extraFields && typeof record.extraFields === 'object') {
|
|
79
|
+
prunePrivateExtraFields(record.extraFields);
|
|
80
|
+
if (!Object.keys(record.extraFields).length) {
|
|
81
|
+
delete record.extraFields;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function prunePrivateExtraFields(target) {
|
|
86
|
+
for (const key of Object.keys(target)) {
|
|
87
|
+
const value = target[key];
|
|
88
|
+
if (typeof key === 'string' && key.startsWith('__rcc_')) {
|
|
89
|
+
delete target[key];
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (value && typeof value === 'object') {
|
|
93
|
+
prunePrivateExtraFields(value);
|
|
94
|
+
if (!Object.keys(value).length) {
|
|
95
|
+
delete target[key];
|
|
96
|
+
}
|
|
97
|
+
}
|
|
73
98
|
}
|
|
74
99
|
}
|
|
100
|
+
const RAW_SYSTEM_SENTINEL = '__rcc_raw_system';
|
|
75
101
|
export function sanitizeResponsesFunctionName(rawName) {
|
|
76
102
|
if (typeof rawName !== 'string') {
|
|
77
103
|
return undefined;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface SnapshotHookOptions {
|
|
2
|
+
endpoint: string;
|
|
3
|
+
stage: string;
|
|
4
|
+
requestId: string;
|
|
5
|
+
data: unknown;
|
|
6
|
+
verbosity?: 'minimal' | 'verbose';
|
|
7
|
+
channel?: string;
|
|
8
|
+
}
|
|
9
|
+
export declare function writeSnapshotViaHooks(options: SnapshotHookOptions): Promise<void>;
|
|
@@ -1,7 +1,61 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
const DEFAULT_SNAPSHOT_ROOT = path.join(os.homedir(), '.routecodex', 'codex-samples');
|
|
5
|
+
function resolveSnapshotRoot() {
|
|
6
|
+
const envOverride = process.env.RCC_SNAPSHOT_DIR ||
|
|
7
|
+
process.env.ROUTECODEX_SNAPSHOT_DIR;
|
|
8
|
+
if (envOverride && envOverride.trim()) {
|
|
9
|
+
return path.resolve(envOverride.trim());
|
|
10
|
+
}
|
|
11
|
+
return DEFAULT_SNAPSHOT_ROOT;
|
|
12
|
+
}
|
|
13
|
+
function resolveSnapshotFolder(endpoint) {
|
|
14
|
+
const lowered = (endpoint || '').toLowerCase();
|
|
15
|
+
if (lowered.includes('/responses')) {
|
|
16
|
+
return 'openai-responses';
|
|
17
|
+
}
|
|
18
|
+
if (lowered.includes('/messages')) {
|
|
19
|
+
return 'anthropic-messages';
|
|
20
|
+
}
|
|
21
|
+
return 'openai-chat';
|
|
22
|
+
}
|
|
23
|
+
function sanitizeToken(value, fallback) {
|
|
24
|
+
if (typeof value !== 'string') {
|
|
25
|
+
return fallback;
|
|
26
|
+
}
|
|
27
|
+
const trimmed = value.trim();
|
|
28
|
+
if (!trimmed) {
|
|
29
|
+
return fallback;
|
|
30
|
+
}
|
|
31
|
+
return trimmed.replace(/[^A-Za-z0-9_.-]/g, '_') || fallback;
|
|
32
|
+
}
|
|
33
|
+
function channelSuffix(channel) {
|
|
34
|
+
if (!channel) {
|
|
35
|
+
return '';
|
|
36
|
+
}
|
|
37
|
+
const token = sanitizeToken(channel, '');
|
|
38
|
+
return token ? `_${token}` : '';
|
|
39
|
+
}
|
|
40
|
+
async function writeSnapshotFile(options) {
|
|
41
|
+
const root = resolveSnapshotRoot();
|
|
42
|
+
const folder = resolveSnapshotFolder(options.endpoint);
|
|
43
|
+
const dir = path.join(root, folder);
|
|
44
|
+
const stageToken = sanitizeToken(options.stage, 'snapshot');
|
|
45
|
+
const requestToken = sanitizeToken(options.requestId, `req_${Date.now()}`);
|
|
46
|
+
const filePath = path.join(dir, `${requestToken}_${stageToken}${channelSuffix(options.channel)}.json`);
|
|
47
|
+
await fs.mkdir(dir, { recursive: true });
|
|
48
|
+
const spacing = options.verbosity === 'minimal' ? undefined : 2;
|
|
49
|
+
const payload = spacing !== undefined
|
|
50
|
+
? JSON.stringify(options.data, null, spacing)
|
|
51
|
+
: JSON.stringify(options.data);
|
|
52
|
+
await fs.writeFile(filePath, payload, 'utf-8');
|
|
53
|
+
}
|
|
54
|
+
export async function writeSnapshotViaHooks(options) {
|
|
55
|
+
try {
|
|
56
|
+
await writeSnapshotFile(options);
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
// snapshot writes must never block callers
|
|
60
|
+
}
|
|
7
61
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
interface SnapshotPayload {
|
|
2
|
+
stage: string;
|
|
3
|
+
requestId: string;
|
|
4
|
+
endpoint?: string;
|
|
5
|
+
data: unknown;
|
|
6
|
+
folderHint?: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function shouldRecordSnapshots(): boolean;
|
|
9
|
+
export declare function recordSnapshot(options: SnapshotPayload): Promise<void>;
|
|
10
|
+
export type SnapshotWriter = (stage: string, payload: unknown) => void;
|
|
11
|
+
export declare function createSnapshotWriter(opts: {
|
|
12
|
+
requestId: string;
|
|
13
|
+
endpoint?: string;
|
|
14
|
+
folderHint?: string;
|
|
15
|
+
}): SnapshotWriter | undefined;
|
|
16
|
+
export {};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import os from 'node:os';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import fs from 'node:fs/promises';
|
|
4
|
+
import { writeSnapshotViaHooks } from './snapshot-hooks.js';
|
|
5
|
+
const SNAPSHOT_BASE = path.join(os.homedir(), '.routecodex', 'golden_samples');
|
|
6
|
+
function mapEndpointToFolder(endpoint, hint) {
|
|
7
|
+
if (hint)
|
|
8
|
+
return hint;
|
|
9
|
+
const ep = String(endpoint || '').toLowerCase();
|
|
10
|
+
if (ep.includes('/responses'))
|
|
11
|
+
return 'openai-responses';
|
|
12
|
+
if (ep.includes('/messages'))
|
|
13
|
+
return 'anthropic-messages';
|
|
14
|
+
if (ep.includes('/gemini'))
|
|
15
|
+
return 'gemini-chat';
|
|
16
|
+
return 'openai-chat';
|
|
17
|
+
}
|
|
18
|
+
async function ensureDir(dir) {
|
|
19
|
+
try {
|
|
20
|
+
await fs.mkdir(dir, { recursive: true });
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
// ignore fs errors
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function sanitize(value) {
|
|
27
|
+
return value.replace(/[^\w.-]/g, '_');
|
|
28
|
+
}
|
|
29
|
+
export function shouldRecordSnapshots() {
|
|
30
|
+
const flag = process.env.ROUTECODEX_HUB_SNAPSHOTS;
|
|
31
|
+
if (flag && flag.trim() === '0') {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
export async function recordSnapshot(options) {
|
|
37
|
+
if (!shouldRecordSnapshots())
|
|
38
|
+
return;
|
|
39
|
+
const endpoint = options.endpoint || '/v1/chat/completions';
|
|
40
|
+
const folder = mapEndpointToFolder(endpoint, options.folderHint);
|
|
41
|
+
const dir = path.join(SNAPSHOT_BASE, folder);
|
|
42
|
+
try {
|
|
43
|
+
await ensureDir(dir);
|
|
44
|
+
const safeStage = sanitize(options.stage);
|
|
45
|
+
const safeRequestId = sanitize(options.requestId);
|
|
46
|
+
const file = path.join(dir, `${safeRequestId}_${safeStage}.json`);
|
|
47
|
+
const payload = {
|
|
48
|
+
meta: {
|
|
49
|
+
stage: options.stage,
|
|
50
|
+
timestamp: Date.now(),
|
|
51
|
+
endpoint
|
|
52
|
+
},
|
|
53
|
+
body: options.data
|
|
54
|
+
};
|
|
55
|
+
await fs.writeFile(file, JSON.stringify(payload, null, 2), 'utf-8');
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
console.warn('[snapshot-utils] failed to write snapshot', error);
|
|
59
|
+
}
|
|
60
|
+
void writeSnapshotViaHooks({
|
|
61
|
+
endpoint,
|
|
62
|
+
stage: options.stage,
|
|
63
|
+
requestId: options.requestId,
|
|
64
|
+
data: options.data,
|
|
65
|
+
verbosity: 'verbose'
|
|
66
|
+
}).catch(() => {
|
|
67
|
+
// ignore hook errors
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
export function createSnapshotWriter(opts) {
|
|
71
|
+
if (!shouldRecordSnapshots()) {
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
const endpoint = opts.endpoint || '/v1/chat/completions';
|
|
75
|
+
return (stage, payload) => {
|
|
76
|
+
void recordSnapshot({
|
|
77
|
+
stage,
|
|
78
|
+
requestId: opts.requestId,
|
|
79
|
+
endpoint,
|
|
80
|
+
folderHint: opts.folderHint,
|
|
81
|
+
data: payload
|
|
82
|
+
});
|
|
83
|
+
};
|
|
84
|
+
}
|