plugin-ai-api 1.0.11 → 1.0.12

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.
@@ -26,6 +26,8 @@ var __copyProps = (to, from, except, desc) => {
26
26
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
27
27
  var chat_completions_exports = {};
28
28
  __export(chat_completions_exports, {
29
+ applyProviderRequestParameters: () => applyProviderRequestParameters,
30
+ getProviderRequestParameters: () => getProviderRequestParameters,
29
31
  handleChatCompletions: () => handleChatCompletions
30
32
  });
31
33
  module.exports = __toCommonJS(chat_completions_exports);
@@ -108,13 +110,15 @@ async function handleChatCompletions(ctx, plugin) {
108
110
  ctx.body = (0, import_openai_format.toOpenAIError)(500, `Provider '${service.provider}' not registered`, "server_error");
109
111
  return;
110
112
  }
113
+ const providerRequestParameters = getProviderRequestParameters(body);
111
114
  const modelOptions = {
112
115
  model: modelId,
113
116
  llmService: service.name
114
117
  };
115
118
  if (body.temperature !== void 0) modelOptions.temperature = body.temperature;
116
119
  if (body.top_p !== void 0) modelOptions.topP = body.top_p;
117
- if (body.max_tokens !== void 0) modelOptions.maxTokens = body.max_tokens;
120
+ if (body.max_completion_tokens !== void 0) modelOptions.maxTokens = body.max_completion_tokens;
121
+ else if (body.max_tokens !== void 0) modelOptions.maxTokens = body.max_tokens;
118
122
  if (body.frequency_penalty !== void 0) modelOptions.frequencyPenalty = body.frequency_penalty;
119
123
  if (body.presence_penalty !== void 0) modelOptions.presencePenalty = body.presence_penalty;
120
124
  if (body.stop !== void 0) modelOptions.stop = body.stop;
@@ -166,11 +170,26 @@ async function handleChatCompletions(ctx, plugin) {
166
170
  });
167
171
  const completionId = (0, import_openai_format.generateCompletionId)();
168
172
  const baseModel = provider.createModel();
169
- const chatModel = bindRequestTools(baseModel, body.tools, body.tool_choice);
173
+ applyProviderRequestParameters(baseModel, providerRequestParameters);
174
+ const chatModel = bindRequestTools(baseModel, body.tools, body.tool_choice, providerRequestParameters);
170
175
  if (stream) {
171
- await handleStreamingCompletion(ctx, chatModel, langchainMessages, completionId, body.model);
176
+ await handleStreamingCompletion(
177
+ ctx,
178
+ chatModel,
179
+ langchainMessages,
180
+ completionId,
181
+ body.model,
182
+ providerRequestParameters
183
+ );
172
184
  } else {
173
- await handleNonStreamingCompletion(ctx, chatModel, langchainMessages, completionId, body.model);
185
+ await handleNonStreamingCompletion(
186
+ ctx,
187
+ chatModel,
188
+ langchainMessages,
189
+ completionId,
190
+ body.model,
191
+ providerRequestParameters
192
+ );
174
193
  }
175
194
  } catch (err) {
176
195
  ctx.log.error("AI API chat completions error:", err);
@@ -180,8 +199,8 @@ async function handleChatCompletions(ctx, plugin) {
180
199
  }
181
200
  }
182
201
  }
183
- async function handleNonStreamingCompletion(ctx, chatModel, messages, completionId, modelName) {
184
- const result = await chatModel.invoke(messages);
202
+ async function handleNonStreamingCompletion(ctx, chatModel, messages, completionId, modelName, providerRequestParameters) {
203
+ const result = await chatModel.invoke(messages, providerRequestParameters);
185
204
  let content = "";
186
205
  if (typeof result.content === "string") {
187
206
  content = result.content;
@@ -204,7 +223,7 @@ async function handleNonStreamingCompletion(ctx, chatModel, messages, completion
204
223
  toolCalls
205
224
  });
206
225
  }
207
- async function handleStreamingCompletion(ctx, chatModel, messages, completionId, modelName) {
226
+ async function handleStreamingCompletion(ctx, chatModel, messages, completionId, modelName, providerRequestParameters) {
208
227
  ctx.set({
209
228
  "Content-Type": "text/event-stream",
210
229
  "Cache-Control": "no-cache",
@@ -227,7 +246,7 @@ async function handleStreamingCompletion(ctx, chatModel, messages, completionId,
227
246
  let usage;
228
247
  let finishReason = "stop";
229
248
  try {
230
- const stream = await chatModel.stream(messages, { signal: requestAbort.signal });
249
+ const stream = await chatModel.stream(messages, { ...providerRequestParameters, signal: requestAbort.signal });
231
250
  for await (const chunk of stream) {
232
251
  if (requestAbort.signal.aborted) throw requestAbort.signal.reason;
233
252
  let content = "";
@@ -300,12 +319,41 @@ async function handleStreamingCompletion(ctx, chatModel, messages, completionId,
300
319
  function getErrorMessage(error, fallback) {
301
320
  return error instanceof Error && error.message ? error.message : fallback;
302
321
  }
303
- function bindRequestTools(chatModel, tools, toolChoice) {
322
+ const GATEWAY_MANAGED_PARAMETERS = /* @__PURE__ */ new Set(["model", "messages", "tools", "tool_choice", "stream", "n"]);
323
+ function getProviderRequestParameters(body) {
324
+ return Object.fromEntries(
325
+ Object.entries(body).filter(([name, value]) => !GATEWAY_MANAGED_PARAMETERS.has(name) && value !== void 0)
326
+ );
327
+ }
328
+ function applyProviderRequestParameters(chatModel, parameters) {
329
+ if (!chatModel || typeof chatModel !== "object") return;
330
+ const model = chatModel;
331
+ const modelKwargs = { ...model.modelKwargs ?? {} };
332
+ if (!Object.hasOwn(parameters, "response_format")) {
333
+ if (isDefaultTextResponseFormat(modelKwargs.response_format)) delete modelKwargs.response_format;
334
+ if (isDefaultResponsesTextFormat(modelKwargs.text)) delete modelKwargs.text;
335
+ }
336
+ model.modelKwargs = { ...modelKwargs, ...parameters };
337
+ }
338
+ function bindRequestTools(chatModel, tools, toolChoice, providerRequestParameters) {
304
339
  if (!Array.isArray(tools) || tools.length === 0) return chatModel;
305
340
  if (typeof chatModel.bindTools !== "function") {
306
341
  throw new Error("The selected LLM provider does not support tool calling");
307
342
  }
308
- return chatModel.bindTools(tools, toolChoice === void 0 ? void 0 : { tool_choice: toolChoice });
343
+ return chatModel.bindTools(tools, {
344
+ ...providerRequestParameters,
345
+ ...toolChoice === void 0 ? {} : { tool_choice: toolChoice }
346
+ });
347
+ }
348
+ function isDefaultTextResponseFormat(value) {
349
+ return isRecord(value) && value.type === "text" && Object.keys(value).length === 1;
350
+ }
351
+ function isDefaultResponsesTextFormat(value) {
352
+ if (!isRecord(value) || !isRecord(value.format)) return false;
353
+ return value.format.type === "text" && Object.keys(value.format).length === 1 && Object.keys(value).length === 1;
354
+ }
355
+ function isRecord(value) {
356
+ return typeof value === "object" && value !== null && !Array.isArray(value);
309
357
  }
310
358
  function normalizeToolCalls(value) {
311
359
  if (!Array.isArray(value) || value.length === 0) return void 0;
@@ -340,5 +388,7 @@ function serializeToolArguments(value) {
340
388
  }
341
389
  // Annotate the CommonJS export names for ESM import in node:
342
390
  0 && (module.exports = {
391
+ applyProviderRequestParameters,
392
+ getProviderRequestParameters,
343
393
  handleChatCompletions
344
394
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "plugin-ai-api",
3
- "version": "1.0.11",
3
+ "version": "1.0.12",
4
4
  "main": "dist/server/index.js",
5
5
  "dependencies": {},
6
6
  "peerDependencies": {
@@ -1,5 +1,6 @@
1
1
  import { toOpenAIResponse, toOpenAIStreamChunk } from '../utils/openai-format';
2
2
  import { isStreamingRequested } from '../utils/streaming';
3
+ import { applyProviderRequestParameters, getProviderRequestParameters } from '../routes/chat-completions';
3
4
 
4
5
  describe('AI API OpenAI tool-call formatting', () => {
5
6
  it('streams by default and only disables streaming for an explicit false value', () => {
@@ -50,3 +51,58 @@ describe('AI API OpenAI tool-call formatting', () => {
50
51
  expect(chunk.choices[0].delta.tool_calls?.[0].function?.name).toBe('get_weather');
51
52
  });
52
53
  });
54
+
55
+ describe('AI API provider parameter forwarding', () => {
56
+ it('forwards model and tool-call parameters not managed by the gateway', () => {
57
+ const parameters = getProviderRequestParameters({
58
+ model: 'service/model',
59
+ messages: [{ role: 'user', content: 'Use a tool' }],
60
+ tools: [{ type: 'function', function: { name: 'search' } }],
61
+ tool_choice: 'auto',
62
+ stream: true,
63
+ n: 1,
64
+ parallel_tool_calls: false,
65
+ reasoning_effort: 'medium',
66
+ max_completion_tokens: 4096,
67
+ seed: 7,
68
+ service_tier: 'default',
69
+ });
70
+
71
+ expect(parameters).toEqual({
72
+ parallel_tool_calls: false,
73
+ reasoning_effort: 'medium',
74
+ max_completion_tokens: 4096,
75
+ seed: 7,
76
+ service_tier: 'default',
77
+ });
78
+ });
79
+
80
+ it('merges passthrough parameters into model kwargs and removes the synthetic text response format', () => {
81
+ const model = {
82
+ modelKwargs: {
83
+ response_format: { type: 'text' },
84
+ existing_provider_option: true,
85
+ },
86
+ };
87
+
88
+ applyProviderRequestParameters(model, {
89
+ parallel_tool_calls: false,
90
+ reasoning_effort: 'high',
91
+ });
92
+
93
+ expect(model.modelKwargs).toEqual({
94
+ existing_provider_option: true,
95
+ parallel_tool_calls: false,
96
+ reasoning_effort: 'high',
97
+ });
98
+ });
99
+
100
+ it('preserves an explicitly requested response format', () => {
101
+ const model = { modelKwargs: { response_format: { type: 'text' } } };
102
+ const responseFormat = { type: 'json_schema', json_schema: { name: 'answer', schema: { type: 'object' } } };
103
+
104
+ applyProviderRequestParameters(model, { response_format: responseFormat });
105
+
106
+ expect(model.modelKwargs.response_format).toEqual(responseFormat);
107
+ });
108
+ });
@@ -120,7 +120,8 @@ export async function handleChatCompletions(ctx: Context, plugin: PluginAiApiSer
120
120
  return;
121
121
  }
122
122
 
123
- const modelOptions: Record<string, any> = {
123
+ const providerRequestParameters = getProviderRequestParameters(body);
124
+ const modelOptions: Record<string, unknown> = {
124
125
  model: modelId,
125
126
  llmService: service.name,
126
127
  };
@@ -128,7 +129,8 @@ export async function handleChatCompletions(ctx: Context, plugin: PluginAiApiSer
128
129
  // Pass through optional parameters
129
130
  if (body.temperature !== undefined) modelOptions.temperature = body.temperature;
130
131
  if (body.top_p !== undefined) modelOptions.topP = body.top_p;
131
- if (body.max_tokens !== undefined) modelOptions.maxTokens = body.max_tokens;
132
+ if (body.max_completion_tokens !== undefined) modelOptions.maxTokens = body.max_completion_tokens;
133
+ else if (body.max_tokens !== undefined) modelOptions.maxTokens = body.max_tokens;
132
134
  if (body.frequency_penalty !== undefined) modelOptions.frequencyPenalty = body.frequency_penalty;
133
135
  if (body.presence_penalty !== undefined) modelOptions.presencePenalty = body.presence_penalty;
134
136
  if (body.stop !== undefined) modelOptions.stop = body.stop;
@@ -192,14 +194,29 @@ export async function handleChatCompletions(ctx: Context, plugin: PluginAiApiSer
192
194
 
193
195
  const completionId = generateCompletionId();
194
196
  const baseModel = provider.createModel();
195
- const chatModel = bindRequestTools(baseModel, body.tools, body.tool_choice);
197
+ applyProviderRequestParameters(baseModel, providerRequestParameters);
198
+ const chatModel = bindRequestTools(baseModel, body.tools, body.tool_choice, providerRequestParameters);
196
199
 
197
200
  if (stream) {
198
201
  // ─── Streaming mode ───
199
- await handleStreamingCompletion(ctx, chatModel, langchainMessages, completionId, body.model);
202
+ await handleStreamingCompletion(
203
+ ctx,
204
+ chatModel,
205
+ langchainMessages,
206
+ completionId,
207
+ body.model,
208
+ providerRequestParameters,
209
+ );
200
210
  } else {
201
211
  // ─── Non-streaming mode ───
202
- await handleNonStreamingCompletion(ctx, chatModel, langchainMessages, completionId, body.model);
212
+ await handleNonStreamingCompletion(
213
+ ctx,
214
+ chatModel,
215
+ langchainMessages,
216
+ completionId,
217
+ body.model,
218
+ providerRequestParameters,
219
+ );
203
220
  }
204
221
  } catch (err) {
205
222
  ctx.log.error('AI API chat completions error:', err);
@@ -218,8 +235,9 @@ async function handleNonStreamingCompletion(
218
235
  messages: any[],
219
236
  completionId: string,
220
237
  modelName: string,
238
+ providerRequestParameters: Record<string, unknown>,
221
239
  ) {
222
- const result = await chatModel.invoke(messages);
240
+ const result = await chatModel.invoke(messages, providerRequestParameters);
223
241
 
224
242
  let content = '';
225
243
  if (typeof result.content === 'string') {
@@ -258,6 +276,7 @@ async function handleStreamingCompletion(
258
276
  messages: any[],
259
277
  completionId: string,
260
278
  modelName: string,
279
+ providerRequestParameters: Record<string, unknown>,
261
280
  ) {
262
281
  // Set SSE headers
263
282
  ctx.set({
@@ -284,7 +303,7 @@ async function handleStreamingCompletion(
284
303
  let usage: { prompt_tokens: number; completion_tokens: number; total_tokens: number } | undefined;
285
304
  let finishReason = 'stop';
286
305
  try {
287
- const stream = await chatModel.stream(messages, { signal: requestAbort.signal });
306
+ const stream = await chatModel.stream(messages, { ...providerRequestParameters, signal: requestAbort.signal });
288
307
 
289
308
  for await (const chunk of stream) {
290
309
  if (requestAbort.signal.aborted) throw requestAbort.signal.reason;
@@ -367,12 +386,62 @@ function getErrorMessage(error: unknown, fallback: string) {
367
386
  return error instanceof Error && error.message ? error.message : fallback;
368
387
  }
369
388
 
370
- function bindRequestTools(chatModel: any, tools: unknown, toolChoice: unknown) {
389
+ const GATEWAY_MANAGED_PARAMETERS = new Set(['model', 'messages', 'tools', 'tool_choice', 'stream', 'n']);
390
+
391
+ export function getProviderRequestParameters(body: Record<string, unknown>): Record<string, unknown> {
392
+ return Object.fromEntries(
393
+ Object.entries(body).filter(([name, value]) => !GATEWAY_MANAGED_PARAMETERS.has(name) && value !== undefined),
394
+ );
395
+ }
396
+
397
+ interface ModelWithKwargs {
398
+ modelKwargs?: Record<string, unknown>;
399
+ }
400
+
401
+ export function applyProviderRequestParameters(chatModel: unknown, parameters: Record<string, unknown>): void {
402
+ if (!chatModel || typeof chatModel !== 'object') return;
403
+
404
+ const model = chatModel as ModelWithKwargs;
405
+ const modelKwargs = { ...(model.modelKwargs ?? {}) };
406
+
407
+ // OpenAI providers currently install a synthetic text response format even when
408
+ // the client did not request one. Remove that default so LLM mode matches the
409
+ // original OpenAI-compatible request more closely.
410
+ if (!Object.hasOwn(parameters, 'response_format')) {
411
+ if (isDefaultTextResponseFormat(modelKwargs.response_format)) delete modelKwargs.response_format;
412
+ if (isDefaultResponsesTextFormat(modelKwargs.text)) delete modelKwargs.text;
413
+ }
414
+
415
+ model.modelKwargs = { ...modelKwargs, ...parameters };
416
+ }
417
+
418
+ function bindRequestTools(
419
+ chatModel: any,
420
+ tools: unknown,
421
+ toolChoice: unknown,
422
+ providerRequestParameters: Record<string, unknown>,
423
+ ) {
371
424
  if (!Array.isArray(tools) || tools.length === 0) return chatModel;
372
425
  if (typeof chatModel.bindTools !== 'function') {
373
426
  throw new Error('The selected LLM provider does not support tool calling');
374
427
  }
375
- return chatModel.bindTools(tools, toolChoice === undefined ? undefined : { tool_choice: toolChoice });
428
+ return chatModel.bindTools(tools, {
429
+ ...providerRequestParameters,
430
+ ...(toolChoice === undefined ? {} : { tool_choice: toolChoice }),
431
+ });
432
+ }
433
+
434
+ function isDefaultTextResponseFormat(value: unknown): boolean {
435
+ return isRecord(value) && value.type === 'text' && Object.keys(value).length === 1;
436
+ }
437
+
438
+ function isDefaultResponsesTextFormat(value: unknown): boolean {
439
+ if (!isRecord(value) || !isRecord(value.format)) return false;
440
+ return value.format.type === 'text' && Object.keys(value.format).length === 1 && Object.keys(value).length === 1;
441
+ }
442
+
443
+ function isRecord(value: unknown): value is Record<string, unknown> {
444
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
376
445
  }
377
446
 
378
447
  function normalizeToolCalls(value: unknown): OpenAIToolCall[] | undefined {