@jsonstudio/llms 0.6.2 → 0.6.6

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,4 +1,5 @@
1
1
  import { captureResponsesContext, buildChatRequestFromResponses } from '../../../../../responses/responses-openai-bridge.js';
2
+ import { captureResponsesRequestContext } from '../../../../../shared/responses-conversation-store.js';
2
3
  import { recordStage } from '../../../stages/utils.js';
3
4
  export async function runReqInboundStage3ContextCapture(options) {
4
5
  let context;
@@ -18,6 +19,18 @@ export async function runReqInboundStage3ContextCapture(options) {
18
19
  ? augmentContextSnapshot(context, fallbackSnapshot)
19
20
  : fallbackSnapshot;
20
21
  recordStage(options.stageRecorder, 'req_inbound_stage3_context_capture', snapshot);
22
+ if (options.adapterContext.providerProtocol === 'openai-responses') {
23
+ try {
24
+ captureResponsesRequestContext({
25
+ requestId: options.adapterContext.requestId,
26
+ payload: options.rawRequest,
27
+ context: snapshot
28
+ });
29
+ }
30
+ catch {
31
+ /* ignore capture failures */
32
+ }
33
+ }
21
34
  return context ?? snapshot;
22
35
  }
23
36
  export function runChatContextCapture(options) {
@@ -9,7 +9,7 @@ export declare class OpenAIChatResponseMapper implements ResponseMapper {
9
9
  toChatCompletion(format: FormatEnvelope, _ctx: AdapterContext): ChatCompletionLike;
10
10
  }
11
11
  export declare class ResponsesResponseMapper implements ResponseMapper {
12
- toChatCompletion(format: FormatEnvelope, _ctx: AdapterContext): ChatCompletionLike;
12
+ toChatCompletion(format: FormatEnvelope, ctx: AdapterContext): ChatCompletionLike;
13
13
  }
14
14
  export declare class AnthropicResponseMapper implements ResponseMapper {
15
15
  toChatCompletion(format: FormatEnvelope, ctx: AdapterContext): ChatCompletionLike;
@@ -1,5 +1,6 @@
1
1
  import { buildOpenAIChatFromGeminiResponse } from '../../codecs/gemini-openai-codec.js';
2
2
  import { buildChatResponseFromResponses } from '../../shared/responses-response-utils.js';
3
+ import { recordResponsesResponse } from '../../shared/responses-conversation-store.js';
3
4
  import { buildOpenAIChatFromAnthropicMessage } from './response-runtime.js';
4
5
  export class OpenAIChatResponseMapper {
5
6
  toChatCompletion(format, _ctx) {
@@ -7,8 +8,17 @@ export class OpenAIChatResponseMapper {
7
8
  }
8
9
  }
9
10
  export class ResponsesResponseMapper {
10
- toChatCompletion(format, _ctx) {
11
- return buildChatResponseFromResponses(format.payload ?? {});
11
+ toChatCompletion(format, ctx) {
12
+ const payload = (format.payload ?? {});
13
+ if (ctx?.requestId) {
14
+ try {
15
+ recordResponsesResponse({ requestId: ctx.requestId, response: payload });
16
+ }
17
+ catch {
18
+ /* ignore capture failures */
19
+ }
20
+ }
21
+ return buildChatResponseFromResponses(payload);
12
22
  }
13
23
  }
14
24
  export class AnthropicResponseMapper {
@@ -0,0 +1,35 @@
1
+ type AnyRecord = Record<string, unknown>;
2
+ interface CaptureContextArgs {
3
+ requestId?: string;
4
+ payload: AnyRecord;
5
+ context: AnyRecord;
6
+ }
7
+ interface RecordResponseArgs {
8
+ requestId?: string;
9
+ response: AnyRecord;
10
+ }
11
+ interface ResumeOptions {
12
+ requestId?: string;
13
+ }
14
+ interface ResumeResult {
15
+ payload: AnyRecord;
16
+ meta: AnyRecord;
17
+ }
18
+ declare class ResponsesConversationStore {
19
+ private requestMap;
20
+ private responseIndex;
21
+ rebindRequestId(oldId: string | undefined, newId: string | undefined): void;
22
+ captureRequestContext(args: CaptureContextArgs): void;
23
+ recordResponse(args: RecordResponseArgs): void;
24
+ resumeConversation(responseId: string, submitPayload: AnyRecord, options?: ResumeOptions): ResumeResult;
25
+ clearRequest(requestId?: string): void;
26
+ private cleanupEntry;
27
+ private prune;
28
+ }
29
+ declare const store: ResponsesConversationStore;
30
+ export declare function captureResponsesRequestContext(args: CaptureContextArgs): void;
31
+ export declare function recordResponsesResponse(args: RecordResponseArgs): void;
32
+ export declare function resumeResponsesConversation(responseId: string, submitPayload: AnyRecord, options?: ResumeOptions): ResumeResult;
33
+ export declare function clearResponsesConversationByRequestId(requestId?: string): void;
34
+ export declare function rebindResponsesConversationRequestId(oldId?: string, newId?: string): void;
35
+ export { store as responsesConversationStore };
@@ -1,4 +1,4 @@
1
- const TTL_MS = 1000 * 60 * 30; // 30 minutes safety window
1
+ const TTL_MS = 1000 * 60 * 30; // 30min
2
2
  function cloneDeep(value) {
3
3
  try {
4
4
  if (typeof globalThis.structuredClone === 'function') {
@@ -49,19 +49,26 @@ function pickPersistedFields(payload) {
49
49
  'input_audio',
50
50
  'output_audio'
51
51
  ];
52
- const result = {};
52
+ const next = {};
53
53
  for (const key of fields) {
54
54
  if (payload[key] !== undefined) {
55
- result[key] = cloneDeep(payload[key]);
55
+ next[key] = cloneDeep(payload[key]);
56
56
  }
57
57
  }
58
- return result;
58
+ return next;
59
59
  }
60
60
  function normalizeOutputItemToInput(item) {
61
+ if (!item || typeof item !== 'object') {
62
+ return null;
63
+ }
61
64
  const type = typeof item.type === 'string' ? item.type : '';
62
65
  if (type === 'message' || type === 'reasoning') {
63
66
  const role = typeof item.role === 'string' ? item.role : 'assistant';
64
- const content = Array.isArray(item.content) ? cloneDeep(item.content) : (typeof item.text === 'string' ? [{ type: 'text', text: item.text }] : []);
67
+ const content = Array.isArray(item.content)
68
+ ? cloneDeep(item.content)
69
+ : typeof item.text === 'string'
70
+ ? [{ type: 'text', text: item.text }]
71
+ : [];
65
72
  return {
66
73
  type: 'message',
67
74
  role,
@@ -70,7 +77,11 @@ function normalizeOutputItemToInput(item) {
70
77
  };
71
78
  }
72
79
  if (type === 'function_call') {
73
- const callId = typeof item.call_id === 'string' ? item.call_id : (typeof item.id === 'string' ? item.id : undefined);
80
+ const callId = typeof item.call_id === 'string'
81
+ ? item.call_id
82
+ : typeof item.id === 'string'
83
+ ? item.id
84
+ : undefined;
74
85
  return {
75
86
  type: 'function_call',
76
87
  role: 'assistant',
@@ -80,9 +91,9 @@ function normalizeOutputItemToInput(item) {
80
91
  arguments: item.arguments,
81
92
  function: isRecord(item.function)
82
93
  ? cloneDeep(item.function)
83
- : (typeof item.name === 'string'
94
+ : typeof item.name === 'string'
84
95
  ? { name: item.name, arguments: item.arguments }
85
- : undefined)
96
+ : undefined
86
97
  };
87
98
  }
88
99
  return null;
@@ -139,6 +150,18 @@ function normalizeSubmittedToolOutputs(toolOutputs) {
139
150
  class ResponsesConversationStore {
140
151
  requestMap = new Map();
141
152
  responseIndex = new Map();
153
+ rebindRequestId(oldId, newId) {
154
+ if (!oldId || !newId || oldId === newId) {
155
+ return;
156
+ }
157
+ const entry = this.requestMap.get(oldId);
158
+ if (!entry) {
159
+ return;
160
+ }
161
+ this.requestMap.delete(oldId);
162
+ entry.requestId = newId;
163
+ this.requestMap.set(newId, entry);
164
+ }
142
165
  captureRequestContext(args) {
143
166
  const { requestId, payload, context } = args;
144
167
  if (!requestId || !payload)
@@ -148,7 +171,8 @@ class ResponsesConversationStore {
148
171
  requestId,
149
172
  basePayload: pickPersistedFields(payload),
150
173
  input: coerceInputArray(context.input),
151
- tools: coerceTools(context.toolsRaw || (Array.isArray(payload.tools) ? payload.tools : undefined)),
174
+ tools: coerceTools(context.toolsRaw) ||
175
+ coerceTools(Array.isArray(payload.tools) ? payload.tools : undefined),
152
176
  createdAt: Date.now(),
153
177
  updatedAt: Date.now()
154
178
  };
@@ -164,7 +188,7 @@ class ResponsesConversationStore {
164
188
  this.requestMap.set(requestId, entry);
165
189
  }
166
190
  recordResponse(args) {
167
- const entry = this.requestMap.get(args.requestId);
191
+ const entry = args.requestId ? this.requestMap.get(args.requestId) : undefined;
168
192
  if (!entry)
169
193
  return;
170
194
  const response = args.response;
@@ -188,9 +212,7 @@ class ResponsesConversationStore {
188
212
  if (!entry) {
189
213
  throw new Error('Responses conversation expired or not found');
190
214
  }
191
- const toolOutputs = Array.isArray(submitPayload.tool_outputs)
192
- ? submitPayload.tool_outputs
193
- : [];
215
+ const toolOutputs = Array.isArray(submitPayload.tool_outputs) ? submitPayload.tool_outputs : [];
194
216
  if (!toolOutputs.length) {
195
217
  throw new Error('tool_outputs array is required when submitting Responses tool results');
196
218
  }
@@ -208,7 +230,8 @@ class ResponsesConversationStore {
208
230
  payload.model = submitPayload.model.trim();
209
231
  }
210
232
  if (submitPayload.metadata && isRecord(submitPayload.metadata)) {
211
- payload.metadata = { ...payload.metadata, ...cloneDeep(submitPayload.metadata) };
233
+ const baseMeta = isRecord(payload.metadata) ? payload.metadata : {};
234
+ payload.metadata = { ...baseMeta, ...cloneDeep(submitPayload.metadata) };
212
235
  }
213
236
  delete payload.tool_outputs;
214
237
  delete payload.response_id;
@@ -225,6 +248,8 @@ class ResponsesConversationStore {
225
248
  };
226
249
  }
227
250
  clearRequest(requestId) {
251
+ if (!requestId)
252
+ return;
228
253
  const entry = this.requestMap.get(requestId);
229
254
  if (!entry)
230
255
  return;
@@ -254,10 +279,14 @@ class ResponsesConversationStore {
254
279
  }
255
280
  }
256
281
  }
257
- export const responsesConversationStore = new ResponsesConversationStore();
282
+ const store = new ResponsesConversationStore();
283
+ const RESPONSES_DEBUG = (process.env.ROUTECODEX_RESPONSES_DEBUG || '').trim() === '1';
258
284
  export function captureResponsesRequestContext(args) {
259
285
  try {
260
- responsesConversationStore.captureRequestContext(args);
286
+ if (RESPONSES_DEBUG) {
287
+ console.log('[responses-store] capture', args.requestId);
288
+ }
289
+ store.captureRequestContext(args);
261
290
  }
262
291
  catch {
263
292
  /* ignore capture failures */
@@ -265,15 +294,31 @@ export function captureResponsesRequestContext(args) {
265
294
  }
266
295
  export function recordResponsesResponse(args) {
267
296
  try {
268
- responsesConversationStore.recordResponse(args);
297
+ if (RESPONSES_DEBUG) {
298
+ console.log('[responses-store] record', args.requestId, args.response?.id);
299
+ }
300
+ store.recordResponse(args);
269
301
  }
270
302
  catch {
271
303
  /* ignore */
272
304
  }
273
305
  }
274
306
  export function resumeResponsesConversation(responseId, submitPayload, options) {
275
- return responsesConversationStore.resumeConversation(responseId, submitPayload, options);
307
+ if (RESPONSES_DEBUG) {
308
+ console.log('[responses-store] resume', responseId);
309
+ }
310
+ return store.resumeConversation(responseId, submitPayload, options);
276
311
  }
277
312
  export function clearResponsesConversationByRequestId(requestId) {
278
- responsesConversationStore.clearRequest(requestId);
313
+ if (RESPONSES_DEBUG && requestId) {
314
+ console.log('[responses-store] clear', requestId);
315
+ }
316
+ store.clearRequest(requestId);
317
+ }
318
+ export function rebindResponsesConversationRequestId(oldId, newId) {
319
+ if (RESPONSES_DEBUG && oldId && newId) {
320
+ console.log('[responses-store] rebind', oldId, '->', newId);
321
+ }
322
+ store.rebindRequestId(oldId, newId);
279
323
  }
324
+ export { store as responsesConversationStore };
@@ -112,29 +112,50 @@ export function buildResponsesOutputFromChat(options) {
112
112
  };
113
113
  }
114
114
  function normalizeUsage(usageRaw) {
115
- if (usageRaw && typeof usageRaw === 'object') {
116
- const usage = { ...usageRaw };
117
- if (usage.input_tokens != null && usage.prompt_tokens == null) {
118
- usage.prompt_tokens = usage.input_tokens;
119
- }
120
- if (usage.output_tokens != null && usage.completion_tokens == null) {
121
- usage.completion_tokens = usage.output_tokens;
122
- }
123
- if (usage.prompt_tokens != null && usage.completion_tokens != null && usage.total_tokens == null) {
124
- const total = Number(usage.prompt_tokens) + Number(usage.completion_tokens);
125
- if (!Number.isNaN(total))
126
- usage.total_tokens = total;
127
- }
128
- try {
129
- delete usage.input_tokens;
130
- delete usage.output_tokens;
131
- }
132
- catch {
133
- /* ignore */
134
- }
135
- return usage;
115
+ if (!usageRaw || typeof usageRaw !== 'object') {
116
+ return undefined;
117
+ }
118
+ const source = usageRaw;
119
+ const usage = {};
120
+ const inputValue = normalizeNumber(source.input_tokens ?? source.prompt_tokens);
121
+ if (inputValue !== undefined) {
122
+ usage.input_tokens = inputValue;
123
+ }
124
+ const outputValue = normalizeNumber(source.output_tokens ?? source.completion_tokens);
125
+ if (outputValue !== undefined) {
126
+ usage.output_tokens = outputValue;
127
+ }
128
+ const totalValue = normalizeNumber(source.total_tokens);
129
+ if (totalValue !== undefined) {
130
+ usage.total_tokens = totalValue;
131
+ }
132
+ else if (inputValue !== undefined && outputValue !== undefined) {
133
+ usage.total_tokens = inputValue + outputValue;
134
+ }
135
+ const inputDetails = extractTokenDetails(source.input_tokens_details ??
136
+ source.prompt_tokens_details);
137
+ if (inputDetails) {
138
+ usage.input_tokens_details = inputDetails;
139
+ }
140
+ const outputDetails = extractTokenDetails(source.output_tokens_details ??
141
+ source.completion_tokens_details);
142
+ if (outputDetails) {
143
+ usage.output_tokens_details = outputDetails;
144
+ }
145
+ return Object.keys(usage).length ? usage : undefined;
146
+ }
147
+ function normalizeNumber(value) {
148
+ if (value == null) {
149
+ return undefined;
150
+ }
151
+ const parsed = Number(value);
152
+ return Number.isFinite(parsed) ? parsed : undefined;
153
+ }
154
+ function extractTokenDetails(raw) {
155
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
156
+ return undefined;
136
157
  }
137
- return usageRaw;
158
+ return { ...raw };
138
159
  }
139
160
  function buildFunctionCallOutput(call, allocateOutputId, sanitizeFunctionName, baseCount, offset) {
140
161
  try {
@@ -167,7 +167,7 @@ export function buildChatResponseFromResponses(payload) {
167
167
  const created = typeof response.created_at === 'number'
168
168
  ? response.created_at
169
169
  : (response.created ?? Math.floor(Date.now() / 1000));
170
- const usage = response.usage;
170
+ const usage = mapResponsesUsageToChat(response.usage);
171
171
  const toolCalls = collectToolCallsFromResponses(response);
172
172
  const { textParts, reasoningParts } = extractOutputSegments(response);
173
173
  const rawReasoningSegments = collectRawReasoningSegments(response);
@@ -237,3 +237,49 @@ export function buildChatResponseFromResponses(payload) {
237
237
  }
238
238
  return chat;
239
239
  }
240
+ function mapResponsesUsageToChat(usageRaw) {
241
+ if (!usageRaw || typeof usageRaw !== 'object') {
242
+ return undefined;
243
+ }
244
+ const source = usageRaw;
245
+ const usage = {};
246
+ const promptValue = toNumber(source.prompt_tokens ?? source.input_tokens);
247
+ if (promptValue !== undefined) {
248
+ usage.prompt_tokens = promptValue;
249
+ }
250
+ const completionValue = toNumber(source.completion_tokens ?? source.output_tokens);
251
+ if (completionValue !== undefined) {
252
+ usage.completion_tokens = completionValue;
253
+ }
254
+ const totalValue = toNumber(source.total_tokens);
255
+ if (totalValue !== undefined) {
256
+ usage.total_tokens = totalValue;
257
+ }
258
+ else if (promptValue !== undefined && completionValue !== undefined) {
259
+ usage.total_tokens = promptValue + completionValue;
260
+ }
261
+ const promptDetails = extractDetailObject(source.prompt_tokens_details ??
262
+ source.input_tokens_details);
263
+ if (promptDetails) {
264
+ usage.prompt_tokens_details = promptDetails;
265
+ }
266
+ const completionDetails = extractDetailObject(source.completion_tokens_details ??
267
+ source.output_tokens_details);
268
+ if (completionDetails) {
269
+ usage.completion_tokens_details = completionDetails;
270
+ }
271
+ return Object.keys(usage).length ? usage : undefined;
272
+ }
273
+ function toNumber(value) {
274
+ if (value == null) {
275
+ return undefined;
276
+ }
277
+ const parsed = Number(value);
278
+ return Number.isFinite(parsed) ? parsed : undefined;
279
+ }
280
+ function extractDetailObject(raw) {
281
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
282
+ return undefined;
283
+ }
284
+ return { ...raw };
285
+ }
@@ -95,10 +95,10 @@ export function extractApplyPatchCallsFromText(text) {
95
95
  continue;
96
96
  let argsStr = '{}';
97
97
  try {
98
- argsStr = JSON.stringify({ patch });
98
+ argsStr = JSON.stringify({ input: patch });
99
99
  }
100
100
  catch {
101
- argsStr = '{"patch":""}';
101
+ argsStr = '{"input":""}';
102
102
  }
103
103
  out.push({ id: genId(), name: 'apply_patch', args: argsStr });
104
104
  }
@@ -12,31 +12,133 @@ function repairArgumentsToString(args) {
12
12
  return String(args);
13
13
  }
14
14
  }
15
+ function extractStringContent(value) {
16
+ if (typeof value === 'string') {
17
+ return value;
18
+ }
19
+ if (Array.isArray(value)) {
20
+ const parts = [];
21
+ for (const entry of value) {
22
+ if (typeof entry === 'string') {
23
+ parts.push(entry);
24
+ }
25
+ else if (entry && typeof entry === 'object') {
26
+ const text = entry.text;
27
+ if (typeof text === 'string') {
28
+ parts.push(text);
29
+ }
30
+ }
31
+ }
32
+ if (parts.length) {
33
+ return parts.join('\n');
34
+ }
35
+ }
36
+ return null;
37
+ }
38
+ function readApplyPatchArgument(args) {
39
+ if (!args || typeof args !== 'object')
40
+ return undefined;
41
+ const input = args.input;
42
+ if (typeof input === 'string' && input.trim().length > 0) {
43
+ return input;
44
+ }
45
+ const legacy = args.patch;
46
+ if (typeof legacy === 'string' && legacy.trim().length > 0) {
47
+ return legacy;
48
+ }
49
+ return undefined;
50
+ }
51
+ function hasApplyPatchInput(argText) {
52
+ if (!argText) {
53
+ return false;
54
+ }
55
+ try {
56
+ const parsed = JSON.parse(argText);
57
+ const content = readApplyPatchArgument(parsed);
58
+ return typeof content === 'string' && content.trim().length > 0;
59
+ }
60
+ catch {
61
+ return false;
62
+ }
63
+ }
64
+ function extractUnifiedDiff(content) {
65
+ if (!content)
66
+ return undefined;
67
+ const begin = content.indexOf('*** Begin Patch');
68
+ const end = content.indexOf('*** End Patch');
69
+ if (begin >= 0 && end > begin) {
70
+ return content.slice(begin, end + '*** End Patch'.length).trim();
71
+ }
72
+ return undefined;
73
+ }
74
+ function createToolCallId() {
75
+ return `call_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
76
+ }
15
77
  export function canonicalizeChatResponseTools(payload) {
16
78
  try {
17
79
  const out = isObject(payload) ? JSON.parse(JSON.stringify(payload)) : payload;
18
80
  const choices = Array.isArray(out?.choices) ? out.choices : [];
19
81
  for (const ch of choices) {
20
82
  const msg = ch && ch.message ? ch.message : undefined;
21
- const tcs = Array.isArray(msg?.tool_calls) ? msg.tool_calls : [];
22
- if (!tcs || !tcs.length)
83
+ const toolCalls = Array.isArray(msg?.tool_calls) ? msg.tool_calls : undefined;
84
+ const hasToolCalls = Array.isArray(toolCalls) && toolCalls.length > 0;
85
+ const originalContentText = extractStringContent(msg?.content);
86
+ const harvestedPatch = typeof originalContentText === 'string' ? extractUnifiedDiff(originalContentText) : undefined;
87
+ if (hasToolCalls) {
88
+ // ensure arguments is string and content is null when tool_calls present
89
+ try {
90
+ if (!ch.finish_reason)
91
+ ch.finish_reason = 'tool_calls';
92
+ }
93
+ catch { /* ignore */ }
94
+ try {
95
+ if (msg && typeof msg === 'object')
96
+ msg.content = null;
97
+ }
98
+ catch { /* ignore */ }
99
+ for (const tc of toolCalls) {
100
+ try {
101
+ const fn = tc && tc.function ? tc.function : undefined;
102
+ if (fn) {
103
+ const repaired = repairArgumentsToString(fn.arguments);
104
+ let nextArgs = repaired;
105
+ try {
106
+ const parsed = JSON.parse(repaired);
107
+ const diffText = readApplyPatchArgument(parsed);
108
+ if (typeof diffText === 'string' && diffText.trim().length > 0) {
109
+ nextArgs = JSON.stringify({ input: diffText });
110
+ }
111
+ }
112
+ catch {
113
+ // fallback to repaired string
114
+ }
115
+ fn.arguments = nextArgs;
116
+ if (typeof fn.name === 'string' &&
117
+ fn.name === 'apply_patch' &&
118
+ !hasApplyPatchInput(fn.arguments) &&
119
+ harvestedPatch) {
120
+ fn.arguments = JSON.stringify({ input: harvestedPatch });
121
+ }
122
+ }
123
+ }
124
+ catch { /* ignore */ }
125
+ }
23
126
  continue;
24
- // ensure arguments is string and content is null when tool_calls present
25
- try {
26
- if (!ch.finish_reason)
27
- ch.finish_reason = 'tool_calls';
28
127
  }
29
- catch { /* ignore */ }
30
- try {
31
- if (msg && typeof msg === 'object')
32
- msg.content = null;
33
- }
34
- catch { /* ignore */ }
35
- for (const tc of tcs) {
128
+ if (harvestedPatch) {
129
+ const id = createToolCallId();
130
+ const toolCall = {
131
+ id,
132
+ type: 'function',
133
+ function: {
134
+ name: 'apply_patch',
135
+ arguments: JSON.stringify({ input: harvestedPatch })
136
+ }
137
+ };
36
138
  try {
37
- const fn = tc && tc.function ? tc.function : undefined;
38
- if (fn)
39
- fn.arguments = repairArgumentsToString(fn.arguments);
139
+ msg.tool_calls = [toolCall];
140
+ msg.content = null;
141
+ ch.finish_reason = 'tool_calls';
40
142
  }
41
143
  catch { /* ignore */ }
42
144
  }
@@ -15,6 +15,28 @@ const DEFAULT_SANITIZER = (value) => {
15
15
  }
16
16
  return undefined;
17
17
  };
18
+ const APPLY_PATCH_TOOL_NAME = 'apply_patch';
19
+ const APPLY_PATCH_ARG_KEY = 'input';
20
+ function isApplyPatchTool(name) {
21
+ return typeof name === 'string' && name.trim() === APPLY_PATCH_TOOL_NAME;
22
+ }
23
+ function buildApplyPatchParameters() {
24
+ return {
25
+ type: 'object',
26
+ properties: {
27
+ [APPLY_PATCH_ARG_KEY]: {
28
+ type: 'string',
29
+ description: 'Unified diff patch content (*** Begin Patch ... *** End Patch)'
30
+ }
31
+ },
32
+ required: [APPLY_PATCH_ARG_KEY],
33
+ additionalProperties: false
34
+ };
35
+ }
36
+ function applyPatchSchemaToFunction(fnNode) {
37
+ fnNode.parameters = buildApplyPatchParameters();
38
+ fnNode.strict = true;
39
+ }
18
40
  function resolveToolName(candidate, options) {
19
41
  const sanitized = options?.sanitizeName?.(candidate);
20
42
  if (typeof sanitized === 'string' && sanitized.trim().length) {
@@ -80,6 +102,9 @@ export function bridgeToolToChatDefinition(rawTool, options) {
80
102
  if (strict !== undefined) {
81
103
  fnOut.strict = strict;
82
104
  }
105
+ if (isApplyPatchTool(name)) {
106
+ applyPatchSchemaToFunction(fnOut);
107
+ }
83
108
  return {
84
109
  type: normalizedType,
85
110
  function: fnOut
@@ -131,6 +156,11 @@ export function chatToolToBridgeDefinition(rawTool, options) {
131
156
  if (strict !== undefined) {
132
157
  fnOut.strict = strict;
133
158
  }
159
+ if (isApplyPatchTool(name)) {
160
+ applyPatchSchemaToFunction(fnOut);
161
+ responseShape.parameters = buildApplyPatchParameters();
162
+ responseShape.strict = true;
163
+ }
134
164
  responseShape.function = fnOut;
135
165
  return responseShape;
136
166
  }
@@ -77,6 +77,8 @@ export declare class ResponsesResponseBuilder {
77
77
  * 处理function_call.start事件
78
78
  */
79
79
  private handleFunctionCallStart;
80
+ private coerceArgumentsChunk;
81
+ private shouldOverrideArguments;
80
82
  /**
81
83
  * 处理function_call.delta事件
82
84
  */
@@ -395,6 +395,36 @@ export class ResponsesResponseBuilder {
395
395
  outputItemState.arguments = '';
396
396
  outputItemState.lastEventTime = event.timestamp;
397
397
  }
398
+ coerceArgumentsChunk(raw) {
399
+ if (typeof raw === 'string') {
400
+ return raw;
401
+ }
402
+ if (raw && typeof raw === 'object') {
403
+ try {
404
+ return JSON.stringify(raw);
405
+ }
406
+ catch {
407
+ return String(raw);
408
+ }
409
+ }
410
+ return undefined;
411
+ }
412
+ shouldOverrideArguments(current, incoming) {
413
+ if (!incoming) {
414
+ return false;
415
+ }
416
+ const trimmed = incoming.trim();
417
+ if (!current || !current.length) {
418
+ return trimmed.length > 0;
419
+ }
420
+ if (!trimmed.length) {
421
+ return false;
422
+ }
423
+ if (trimmed === '{}' || trimmed.toLowerCase() === 'null') {
424
+ return false;
425
+ }
426
+ return true;
427
+ }
398
428
  /**
399
429
  * 处理function_call.delta事件
400
430
  */
@@ -404,10 +434,10 @@ export class ResponsesResponseBuilder {
404
434
  if (!outputItemState) {
405
435
  throw new Error(`Output item not found: ${data.item_id}`);
406
436
  }
407
- const chunk = (data && data.delta && typeof data.delta === 'object' && typeof data.delta.arguments === 'string')
408
- ? String(data.delta.arguments)
409
- : (typeof (data?.delta) === 'string' ? String(data.delta) : (typeof data?.arguments === 'string' ? String(data.arguments) : undefined));
410
- if (typeof chunk === 'string') {
437
+ const chunk = this.coerceArgumentsChunk(data?.delta?.arguments) ??
438
+ this.coerceArgumentsChunk(data?.delta) ??
439
+ this.coerceArgumentsChunk(data?.arguments);
440
+ if (chunk) {
411
441
  outputItemState.arguments = (outputItemState.arguments || '') + chunk;
412
442
  }
413
443
  outputItemState.lastEventTime = event.timestamp;
@@ -445,8 +475,16 @@ export class ResponsesResponseBuilder {
445
475
  outputItemState.name = data.name;
446
476
  if (typeof data.call_id === 'string' && data.call_id)
447
477
  outputItemState.callId = data.call_id;
448
- if (typeof data.arguments === 'string' && data.arguments)
449
- outputItemState.arguments = data.arguments;
478
+ const finalChunk = this.coerceArgumentsChunk(data?.arguments) ??
479
+ this.coerceArgumentsChunk(data?.delta?.arguments) ??
480
+ this.coerceArgumentsChunk(data?.delta);
481
+ if (this.shouldOverrideArguments(outputItemState.arguments, finalChunk)) {
482
+ outputItemState.arguments = finalChunk;
483
+ }
484
+ else if (!outputItemState.arguments && finalChunk) {
485
+ // 没有任何累计增量时,保底写入 done 事件里的值
486
+ outputItemState.arguments = finalChunk;
487
+ }
450
488
  }
451
489
  catch { /* ignore */ }
452
490
  outputItemState.status = 'completed';
@@ -630,14 +668,17 @@ export class ResponsesResponseBuilder {
630
668
  this.state = 'error';
631
669
  }
632
670
  handleResponseCompleted(event) {
633
- const data = event.data;
634
- if (data?.usage) {
635
- this.response.usage = data.usage;
671
+ const payload = event.data?.response ?? event.data;
672
+ const usage = (payload && payload.usage)
673
+ ? payload.usage
674
+ : event.data?.usage;
675
+ if (usage) {
676
+ this.response.usage = usage;
636
677
  }
637
678
  // 标准化完成态:部分上游的 response.completed 不包含 status 字段
638
679
  // 若未提供明确的完成状态,统一标记为 'completed',而不是沿用之前的 in_progress。
639
- this.response.status = (data && data.status != null)
640
- ? data.status
680
+ this.response.status = (payload && payload.status != null)
681
+ ? payload.status
641
682
  : 'completed';
642
683
  // 将已聚合的输出写回并标记完成(若为空数组也重建)
643
684
  try {
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@jsonstudio/llms",
3
- "version": "0.6.002",
3
+ "version": "0.6.006",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",
7
7
  "types": "dist/index.d.ts",
8
8
  "scripts": {
9
- "build": "tsc -p tsconfig.json",
10
- "build:dev": "tsc -p tsconfig.json",
9
+ "build": "node scripts/bump-version.mjs && tsc -p tsconfig.json",
10
+ "build:dev": "node scripts/bump-version.mjs && tsc -p tsconfig.json",
11
11
  "lint": "eslint --no-eslintrc -c .eslintrc.json src --ext .ts --no-cache",
12
12
  "lint:fix": "eslint --no-eslintrc -c .eslintrc.json src --ext .ts --no-cache --fix",
13
13
  "postbuild": "node scripts/tests/run-matrix-ci.mjs",