@jsonstudio/llms 0.6.54 → 0.6.74

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.
@@ -1,5 +1,6 @@
1
1
  import { buildResponsesPayloadFromChat, runStandardChatRequestFilters } from '../index.js';
2
- import { captureResponsesContext, buildChatRequestFromResponses } from '../responses/responses-openai-bridge.js';
2
+ import { captureResponsesContext, buildChatRequestFromResponses, buildResponsesRequestFromChat } from '../responses/responses-openai-bridge.js';
3
+ import { captureResponsesRequestContext } from '../shared/responses-conversation-store.js';
3
4
  import { FilterEngine, ResponseToolTextCanonicalizeFilter, ResponseToolArgumentsStringifyFilter, ResponseFinishInvariantsFilter } from '../../filters/index.js';
4
5
  // Ported from root package (no behavior change). Types relaxed.
5
6
  export class ResponsesOpenAIConversionCodec {
@@ -83,6 +84,20 @@ export class ResponsesOpenAIConversionCodec {
83
84
  endpoint: context.endpoint ?? dto.metadata?.endpoint
84
85
  };
85
86
  const filtered = await runStandardChatRequestFilters(chatRequest, profile, ctxForFilters);
87
+ try {
88
+ const rebuilt = buildResponsesRequestFromChat(filtered, ctx);
89
+ const payloadForStore = rebuilt?.request;
90
+ if (payloadForStore && typeof payloadForStore === 'object') {
91
+ captureResponsesRequestContext({
92
+ requestId: dto.route.requestId,
93
+ payload: payloadForStore,
94
+ context: ctx
95
+ });
96
+ }
97
+ }
98
+ catch {
99
+ // best-effort capture
100
+ }
86
101
  if (filtered && typeof filtered === 'object') {
87
102
  const maybe = filtered;
88
103
  if (maybe.max_tokens === undefined && typeof maybe.max_output_tokens === 'number') {
@@ -1,5 +1,4 @@
1
1
  import { captureResponsesContext, buildChatRequestFromResponses } from '../../../../../responses/responses-openai-bridge.js';
2
- import { captureResponsesRequestContext } from '../../../../../shared/responses-conversation-store.js';
3
2
  import { recordStage } from '../../../stages/utils.js';
4
3
  export async function runReqInboundStage3ContextCapture(options) {
5
4
  let context;
@@ -54,16 +53,6 @@ export function captureResponsesContextSnapshot(options) {
54
53
  catch {
55
54
  // best-effort context capture
56
55
  }
57
- try {
58
- captureResponsesRequestContext({
59
- requestId: options.adapterContext.requestId,
60
- payload: options.rawRequest,
61
- context
62
- });
63
- }
64
- catch {
65
- // ignore store capture failures
66
- }
67
56
  return context;
68
57
  }
69
58
  function captureChatContextSnapshot(options) {
@@ -48,6 +48,7 @@ export declare function buildResponsesRequestFromChat(payload: Record<string, un
48
48
  bridgeHistory?: BridgeInputBuildResult;
49
49
  systemInstruction?: string;
50
50
  }): BuildResponsesRequestResult;
51
+ export declare function ensureResponsesApplyPatchArguments(input?: BridgeInputItem[]): void;
51
52
  export declare function buildResponsesPayloadFromChat(payload: unknown, context?: ResponsesRequestContext): Record<string, unknown> | unknown;
52
53
  export declare function extractRequestIdFromResponse(response: any): string | undefined;
53
54
  export { buildChatResponseFromResponses } from '../shared/responses-response-utils.js';
@@ -18,6 +18,7 @@ function isObject(v) {
18
18
  // --- Public bridge functions ---
19
19
  export function captureResponsesContext(payload, dto) {
20
20
  const preservedInput = cloneBridgeEntries(payload.input);
21
+ ensureResponsesApplyPatchArguments(preservedInput);
21
22
  ensureBridgeInstructions(payload);
22
23
  const context = {
23
24
  requestId: dto?.route?.requestId,
@@ -298,6 +299,76 @@ export function buildResponsesRequestFromChat(payload, ctx, extras) {
298
299
  ensureBridgeInstructions(out);
299
300
  return { request: out, originalSystemMessages };
300
301
  }
302
+ export function ensureResponsesApplyPatchArguments(input) {
303
+ if (!Array.isArray(input) || !input.length) {
304
+ return;
305
+ }
306
+ for (const entry of input) {
307
+ if (!entry || typeof entry !== 'object')
308
+ continue;
309
+ const type = typeof entry.type === 'string' ? entry.type.toLowerCase() : '';
310
+ if (type !== 'function_call')
311
+ continue;
312
+ const name = (typeof entry.name === 'string' && entry.name.trim()) ||
313
+ (entry.function && typeof entry.function === 'object' && typeof entry.function.name === 'string' && entry.function.name.trim()) ||
314
+ '';
315
+ if (name !== 'apply_patch')
316
+ continue;
317
+ let normalized;
318
+ try {
319
+ normalized = normalizeApplyPatchArguments(entry.arguments ?? entry.function?.arguments);
320
+ }
321
+ catch {
322
+ // best-effort: do not fail the whole request due to a malformed historical tool call
323
+ normalized = undefined;
324
+ }
325
+ if (normalized === undefined) {
326
+ continue;
327
+ }
328
+ entry.arguments = normalized;
329
+ if (entry.function && typeof entry.function === 'object') {
330
+ entry.function.arguments = normalized;
331
+ }
332
+ }
333
+ }
334
+ function normalizeApplyPatchArguments(source) {
335
+ let parsed;
336
+ if (typeof source === 'string' && source.trim()) {
337
+ try {
338
+ parsed = JSON.parse(source);
339
+ }
340
+ catch {
341
+ parsed = { patch: source };
342
+ }
343
+ }
344
+ else if (source && typeof source === 'object') {
345
+ parsed = { ...source };
346
+ }
347
+ else if (source === undefined) {
348
+ parsed = {};
349
+ }
350
+ else {
351
+ return typeof source === 'string' ? source : undefined;
352
+ }
353
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
354
+ return typeof source === 'string' ? source : undefined;
355
+ }
356
+ const patchText = typeof parsed.patch === 'string' && parsed.patch.trim().length
357
+ ? parsed.patch
358
+ : typeof parsed.input === 'string' && parsed.input.trim().length
359
+ ? parsed.input
360
+ : undefined;
361
+ if (patchText) {
362
+ parsed.patch = patchText;
363
+ parsed.input = patchText;
364
+ }
365
+ try {
366
+ return JSON.stringify(parsed);
367
+ }
368
+ catch {
369
+ return typeof source === 'string' ? source : undefined;
370
+ }
371
+ }
301
372
  function readToolCallIdStyleFromContext(ctx) {
302
373
  if (!ctx) {
303
374
  return undefined;
@@ -1,6 +1,30 @@
1
1
  import { FilterEngine } from '../../filters/index.js';
2
2
  import { loadFieldMapConfig } from '../../filters/utils/fieldmap-loader.js';
3
3
  import { createSnapshotWriter } from './snapshot-utils.js';
4
+ const REQUEST_FILTER_STAGES = [
5
+ 'request_pre',
6
+ 'request_map',
7
+ 'request_post',
8
+ 'request_finalize'
9
+ ];
10
+ const RESPONSE_FILTER_STAGES = [
11
+ 'response_pre',
12
+ 'response_map',
13
+ 'response_post',
14
+ 'response_finalize'
15
+ ];
16
+ function assertStageCoverage(label, registeredStages, skeletonStages) {
17
+ const allowed = new Set(skeletonStages);
18
+ const uncovered = [];
19
+ for (const stage of registeredStages) {
20
+ if (!allowed.has(stage)) {
21
+ uncovered.push(stage);
22
+ }
23
+ }
24
+ if (uncovered.length) {
25
+ throw new Error(`[tool-filter-pipeline] ${label}: registered filter stage(s) not covered by skeleton: ${uncovered.join(', ')}`);
26
+ }
27
+ }
4
28
  export async function runChatRequestToolFilters(chatRequest, options = {}) {
5
29
  const reqCtxBase = {
6
30
  requestId: options.requestId ?? `req_${Date.now()}`,
@@ -23,22 +47,27 @@ export async function runChatRequestToolFilters(chatRequest, options = {}) {
23
47
  };
24
48
  recordStage('req_process_tool_filters_input', chatRequest);
25
49
  const engine = new FilterEngine();
50
+ const registeredStages = new Set();
51
+ const register = (filter) => {
52
+ registeredStages.add(filter.stage);
53
+ engine.registerFilter(filter);
54
+ };
26
55
  const profile = (reqCtxBase.profile || '').toLowerCase();
27
56
  const endpoint = (reqCtxBase.endpoint || '').toLowerCase();
28
57
  const isAnthropic = profile === 'anthropic-messages' || endpoint.includes('/v1/messages');
29
58
  if (!isAnthropic) {
30
59
  try {
31
60
  const { RequestToolListFilter } = await import('../../filters/index.js');
32
- engine.registerFilter(new RequestToolListFilter());
61
+ register(new RequestToolListFilter());
33
62
  }
34
63
  catch {
35
64
  /* optional */
36
65
  }
37
66
  }
38
67
  const { RequestToolCallsStringifyFilter, RequestToolChoicePolicyFilter } = await import('../../filters/index.js');
39
- engine.registerFilter(new RequestToolCallsStringifyFilter());
68
+ register(new RequestToolCallsStringifyFilter());
40
69
  if (!isAnthropic) {
41
- engine.registerFilter(new RequestToolChoicePolicyFilter());
70
+ register(new RequestToolChoicePolicyFilter());
42
71
  }
43
72
  try {
44
73
  const cfg = await loadFieldMapConfig('openai-openai.fieldmap.json');
@@ -52,12 +81,20 @@ export async function runChatRequestToolFilters(chatRequest, options = {}) {
52
81
  } })());
53
82
  }
54
83
  catch { /* ignore */ }
55
- let staged = await engine.run('request_pre', chatRequest, reqCtxBase);
56
- recordStage('req_process_tool_filters_request_pre', staged);
57
- staged = await engine.run('request_map', staged, reqCtxBase);
58
- recordStage('req_process_tool_filters_request_map', staged);
59
- staged = await engine.run('request_post', staged, reqCtxBase);
60
- recordStage('req_process_tool_filters_request_post', staged);
84
+ try {
85
+ const { RequestOpenAIToolsNormalizeFilter, ToolPostConstraintsFilter } = await import('../../filters/index.js');
86
+ register(new RequestOpenAIToolsNormalizeFilter());
87
+ register(new ToolPostConstraintsFilter('request_finalize'));
88
+ }
89
+ catch {
90
+ // optional; keep prior behavior when filter not available
91
+ }
92
+ assertStageCoverage('request', registeredStages, REQUEST_FILTER_STAGES);
93
+ let staged = chatRequest;
94
+ for (const stage of REQUEST_FILTER_STAGES) {
95
+ staged = await engine.run(stage, staged, reqCtxBase);
96
+ recordStage(`req_process_tool_filters_${stage}`, staged);
97
+ }
61
98
  recordStage('req_process_tool_filters_output', staged);
62
99
  return staged;
63
100
  }
@@ -81,20 +118,25 @@ export async function runChatResponseToolFilters(chatJson, options = {}) {
81
118
  };
82
119
  recordStage('resp_process_tool_filters_input', chatJson);
83
120
  const engine = new FilterEngine();
121
+ const registeredStages = new Set();
122
+ const register = (filter) => {
123
+ registeredStages.add(filter.stage);
124
+ engine.registerFilter(filter);
125
+ };
84
126
  const { ResponseToolTextCanonicalizeFilter, ResponseToolArgumentsStringifyFilter, ResponseFinishInvariantsFilter } = await import('../../filters/index.js');
85
- engine.registerFilter(new ResponseToolTextCanonicalizeFilter());
127
+ register(new ResponseToolTextCanonicalizeFilter());
86
128
  try {
87
129
  const { ResponseToolArgumentsToonDecodeFilter, ResponseToolArgumentsBlacklistFilter, ResponseToolArgumentsSchemaConvergeFilter } = await import('../../filters/index.js');
88
- engine.registerFilter(new ResponseToolArgumentsToonDecodeFilter());
130
+ register(new ResponseToolArgumentsToonDecodeFilter());
89
131
  try {
90
- engine.registerFilter(new ResponseToolArgumentsSchemaConvergeFilter());
132
+ register(new ResponseToolArgumentsSchemaConvergeFilter());
91
133
  }
92
134
  catch { /* optional */ }
93
- engine.registerFilter(new ResponseToolArgumentsBlacklistFilter());
135
+ register(new ResponseToolArgumentsBlacklistFilter());
94
136
  }
95
137
  catch { /* optional */ }
96
- engine.registerFilter(new ResponseToolArgumentsStringifyFilter());
97
- engine.registerFilter(new ResponseFinishInvariantsFilter());
138
+ register(new ResponseToolArgumentsStringifyFilter());
139
+ register(new ResponseFinishInvariantsFilter());
98
140
  try {
99
141
  const cfg = await loadFieldMapConfig('openai-openai.fieldmap.json');
100
142
  if (cfg)
@@ -107,12 +149,12 @@ export async function runChatResponseToolFilters(chatJson, options = {}) {
107
149
  } })());
108
150
  }
109
151
  catch { /* ignore */ }
110
- let staged = await engine.run('response_pre', chatJson, resCtxBase);
111
- recordStage('resp_process_tool_filters_response_pre', staged);
112
- staged = await engine.run('response_map', staged, resCtxBase);
113
- recordStage('resp_process_tool_filters_response_map', staged);
114
- staged = await engine.run('response_post', staged, resCtxBase);
115
- recordStage('resp_process_tool_filters_response_post', staged);
152
+ assertStageCoverage('response', registeredStages, RESPONSE_FILTER_STAGES);
153
+ let staged = chatJson;
154
+ for (const stage of RESPONSE_FILTER_STAGES) {
155
+ staged = await engine.run(stage, staged, resCtxBase);
156
+ recordStage(`resp_process_tool_filters_${stage}`, staged);
157
+ }
116
158
  recordStage('resp_process_tool_filters_output', staged);
117
159
  return staged;
118
160
  }
@@ -8,6 +8,56 @@ export function stringifyArgs(args) {
8
8
  return String(args);
9
9
  }
10
10
  }
11
+ function isPlainObject(value) {
12
+ return !!value && typeof value === 'object' && !Array.isArray(value);
13
+ }
14
+ function clonePlainObject(value) {
15
+ try {
16
+ return JSON.parse(JSON.stringify(value));
17
+ }
18
+ catch {
19
+ return { ...value };
20
+ }
21
+ }
22
+ function asSchema(value) {
23
+ if (isPlainObject(value)) {
24
+ return clonePlainObject(value);
25
+ }
26
+ return undefined;
27
+ }
28
+ function ensureApplyPatchSchema(seed) {
29
+ const schema = seed ? { ...seed } : {};
30
+ schema.type = typeof schema.type === 'string' ? schema.type : 'object';
31
+ const properties = isPlainObject(schema.properties) ? { ...schema.properties } : {};
32
+ properties.input = {
33
+ type: 'string',
34
+ description: 'Unified diff patch body between *** Begin Patch/*** End Patch.'
35
+ };
36
+ if (!properties.patch || typeof properties.patch !== 'object') {
37
+ properties.patch = {
38
+ type: 'string',
39
+ description: 'Alias of input for backwards compatibility.'
40
+ };
41
+ }
42
+ schema.properties = properties;
43
+ const requiredList = Array.isArray(schema.required) ? schema.required.filter((entry) => typeof entry === 'string') : [];
44
+ if (!requiredList.includes('input')) {
45
+ requiredList.push('input');
46
+ }
47
+ schema.required = requiredList;
48
+ if (typeof schema.additionalProperties !== 'boolean') {
49
+ schema.additionalProperties = false;
50
+ }
51
+ return schema;
52
+ }
53
+ function enforceBuiltinToolSchema(name, candidate) {
54
+ const normalizedName = typeof name === 'string' ? name.trim().toLowerCase() : '';
55
+ if (normalizedName === 'apply_patch') {
56
+ const base = asSchema(candidate);
57
+ return ensureApplyPatchSchema(base);
58
+ }
59
+ return asSchema(candidate);
60
+ }
11
61
  const DEFAULT_SANITIZER = (value) => {
12
62
  if (typeof value === 'string') {
13
63
  const trimmed = value.trim();
@@ -66,7 +116,7 @@ export function bridgeToolToChatDefinition(rawTool, options) {
66
116
  return null;
67
117
  }
68
118
  const description = resolveToolDescription(fnNode?.description ?? tool.description);
69
- const parameters = resolveToolParameters(fnNode, tool);
119
+ const parameters = enforceBuiltinToolSchema(name, resolveToolParameters(fnNode, tool));
70
120
  const strict = resolveToolStrict(fnNode, tool);
71
121
  const rawType = typeof tool.type === 'string' && tool.type.trim().length ? tool.type.trim() : 'function';
72
122
  const normalizedType = rawType.toLowerCase() === 'custom' ? 'function' : rawType;
@@ -105,7 +155,7 @@ export function chatToolToBridgeDefinition(rawTool, options) {
105
155
  return null;
106
156
  }
107
157
  const description = resolveToolDescription(fnNode?.description);
108
- const parameters = resolveToolParameters(fnNode, undefined);
158
+ const parameters = enforceBuiltinToolSchema(name, resolveToolParameters(fnNode, undefined));
109
159
  const strict = resolveToolStrict(fnNode, undefined);
110
160
  const normalizedType = typeof tool.type === 'string' && tool.type.trim().length ? tool.type.trim() : 'function';
111
161
  const responseShape = {
@@ -73,7 +73,7 @@ export class RequestOpenAIToolsNormalizeFilter {
73
73
  delete dst.function.strict;
74
74
  }
75
75
  catch { /* ignore */ }
76
- // Switch schema for 'shell' at unified shaping point
76
+ // Switch schema for specific built-in tools at unified shaping point
77
77
  try {
78
78
  const name = String(dst.function.name || '').toLowerCase();
79
79
  if (name === 'shell') {
@@ -101,6 +101,25 @@ export class RequestOpenAIToolsNormalizeFilter {
101
101
  };
102
102
  }
103
103
  }
104
+ else if (name === 'apply_patch') {
105
+ dst.function.parameters = {
106
+ type: 'object',
107
+ properties: {
108
+ input: {
109
+ type: 'string',
110
+ description: 'Unified diff patch body between *** Begin Patch/*** End Patch.'
111
+ },
112
+ patch: {
113
+ type: 'string',
114
+ description: 'Alias of input for backwards compatibility.'
115
+ }
116
+ },
117
+ required: ['input'],
118
+ additionalProperties: false
119
+ };
120
+ dst.function.description =
121
+ 'Use apply_patch to edit files. Provide the diff via the input field (*** Begin Patch ... *** End Patch).';
122
+ }
104
123
  }
105
124
  catch { /* ignore */ }
106
125
  finalTools.push(dst);
@@ -95,11 +95,12 @@ export function validateToolCall(name, argsString) {
95
95
  const rawArgs = tryParseJson(typeof argsString === 'string' ? argsString : '{}');
96
96
  switch (normalizedName) {
97
97
  case 'apply_patch': {
98
- const patch = asString(rawArgs.patch);
98
+ const input = asString(rawArgs.input);
99
+ const patch = asString(rawArgs.patch) ?? input;
99
100
  if (!patch) {
100
- return { ok: false, reason: 'missing_patch' };
101
+ return { ok: false, reason: 'missing_input' };
101
102
  }
102
- return { ok: true, normalizedArgs: toJson({ patch }) };
103
+ return { ok: true, normalizedArgs: toJson({ input: patch, patch }) };
103
104
  }
104
105
  case 'shell': {
105
106
  const rawCommand = rawArgs.command;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsonstudio/llms",
3
- "version": "0.6.054",
3
+ "version": "0.6.074",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",