plugin-ai-api 1.1.1 → 1.1.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.
Files changed (108) hide show
  1. package/README.md +51 -12
  2. package/dist/client/185.c47663fefaeb0e5b.js +10 -0
  3. package/dist/client/562.9012cfd1fa04303d.js +10 -0
  4. package/dist/client/685.b5b1e0a5b825d253.js +10 -0
  5. package/dist/client/index.js +1 -1
  6. package/dist/client-v2/185.b552dc91ec2371ba.js +10 -0
  7. package/dist/client-v2/562.db2984167250b1be.js +10 -0
  8. package/dist/client-v2/685.cf16e5b829e06f85.js +10 -0
  9. package/dist/client-v2/index.js +1 -1
  10. package/dist/externalVersion.js +8 -8
  11. package/dist/locale/en-US.json +175 -139
  12. package/dist/locale/vi-VN.json +40 -2
  13. package/dist/locale/zh-CN.json +40 -2
  14. package/dist/server/collections/ai-api-model-metadata.js +26 -0
  15. package/dist/server/collections/ai-api-response-records.js +101 -0
  16. package/dist/server/collections/ai-api-virtual-models.js +68 -0
  17. package/dist/server/middleware/response-record-resource.js +66 -0
  18. package/dist/server/middleware/role-permission.js +43 -18
  19. package/dist/server/migrations/20260901000000-remove-default-group-members.js +60 -0
  20. package/dist/server/migrations/20260902000000-seed-default-role-permissions.js +55 -0
  21. package/dist/server/migrations/20260903000000-seed-sample-response-records.js +170 -0
  22. package/dist/server/plugin.js +66 -16
  23. package/dist/server/routes/chat-completions.js +38 -6
  24. package/dist/server/routes/completions.js +16 -4
  25. package/dist/server/routes/embeddings.js +25 -6
  26. package/dist/server/routes/models.js +29 -0
  27. package/dist/server/routes/responses.js +530 -0
  28. package/dist/server/routes/router.js +65 -10
  29. package/dist/server/usage.js +25 -4
  30. package/dist/server/utils/direct-llm-context.js +1 -1
  31. package/dist/server/utils/resolve-service.js +24 -0
  32. package/dist/server/utils/response-store.js +138 -0
  33. package/dist/server/utils/responses-format.js +686 -0
  34. package/dist/server/utils/responses-stream.js +330 -0
  35. package/dist/server/utils/virtual-models.js +238 -0
  36. package/dist/server/validation.js +44 -2
  37. package/dist/swagger.js +137 -0
  38. package/package.json +34 -32
  39. package/src/__tests__/locale.test.ts +43 -0
  40. package/src/client/__tests__/settings-registration.test.tsx +1 -0
  41. package/src/client/plugin.tsx +9 -1
  42. package/src/client-v2/__tests__/settings-registration.test.tsx +1 -0
  43. package/src/client-v2/pages/ModelMetadataPage.tsx +44 -0
  44. package/src/client-v2/pages/ModelRoutingPage.tsx +238 -0
  45. package/src/client-v2/pages/UsageGroupsPage.tsx +75 -38
  46. package/src/client-v2/plugin.tsx +8 -0
  47. package/src/locale/en-US.json +175 -139
  48. package/src/locale/vi-VN.json +40 -2
  49. package/src/locale/zh-CN.json +40 -2
  50. package/src/server/__tests__/embeddings.test.ts +184 -0
  51. package/src/server/__tests__/models.test.ts +21 -1
  52. package/src/server/__tests__/response-record-resource.test.ts +50 -0
  53. package/src/server/__tests__/response-store-integration.test.ts +341 -0
  54. package/src/server/__tests__/response-store.test.ts +195 -0
  55. package/src/server/__tests__/responses-contract.test.ts +469 -0
  56. package/src/server/__tests__/responses-format.test.ts +299 -0
  57. package/src/server/__tests__/responses-router.test.ts +182 -0
  58. package/src/server/__tests__/responses-streaming.test.ts +368 -0
  59. package/src/server/__tests__/responses.test.ts +462 -0
  60. package/src/server/__tests__/role-permission.test.ts +139 -0
  61. package/src/server/__tests__/seed-role-permission.test.ts +88 -0
  62. package/src/server/__tests__/types/responses-sdk.types.test-d.ts +23 -0
  63. package/src/server/__tests__/usage-groups.test.ts +96 -0
  64. package/src/server/__tests__/usage-route.test.ts +1 -0
  65. package/src/server/__tests__/usage.test.ts +14 -0
  66. package/src/server/__tests__/validation.test.ts +66 -7
  67. package/src/server/__tests__/virtual-model-routing.test.ts +589 -0
  68. package/src/server/collections/ai-api-model-metadata.ts +26 -0
  69. package/src/server/collections/ai-api-response-records.ts +77 -0
  70. package/src/server/collections/ai-api-virtual-models.ts +58 -0
  71. package/src/server/middleware/response-record-resource.ts +44 -0
  72. package/src/server/middleware/role-permission.ts +69 -35
  73. package/src/server/migrations/20260901000000-remove-default-group-members.ts +56 -0
  74. package/src/server/migrations/20260902000000-seed-default-role-permissions.ts +46 -0
  75. package/src/server/migrations/20260903000000-seed-sample-response-records.ts +162 -0
  76. package/src/server/plugin.ts +84 -20
  77. package/src/server/resource/ai-api-config.ts +2 -1
  78. package/src/server/routes/agent-completions.ts +3 -0
  79. package/src/server/routes/chat-completions.ts +34 -10
  80. package/src/server/routes/completions.ts +16 -4
  81. package/src/server/routes/embeddings.ts +32 -10
  82. package/src/server/routes/models.ts +34 -0
  83. package/src/server/routes/responses.ts +640 -0
  84. package/src/server/routes/router.ts +81 -12
  85. package/src/server/services/__tests__/file-processor.test.ts +1 -0
  86. package/src/server/usage.ts +29 -2
  87. package/src/server/utils/app-observability.ts +1 -1
  88. package/src/server/utils/direct-llm-context.ts +2 -1
  89. package/src/server/utils/openai-format.ts +1 -0
  90. package/src/server/utils/resolve-service.ts +39 -1
  91. package/src/server/utils/response-store.ts +148 -0
  92. package/src/server/utils/responses-format.ts +974 -0
  93. package/src/server/utils/responses-stream.ts +384 -0
  94. package/src/server/utils/virtual-models.ts +320 -0
  95. package/src/server/validation.ts +49 -0
  96. package/src/swagger.ts +139 -0
  97. package/dist/client/562.44b16aad4718b4c7.js +0 -10
  98. package/dist/client/685.ae483e17b6b49c98.js +0 -10
  99. package/dist/client-v2/562.45d5c504433be38b.js +0 -10
  100. package/dist/client-v2/685.1030370b309b7d4b.js +0 -10
  101. package/dist/server/collections/ai-api-user-permissions.js +0 -67
  102. package/dist/server/collections/ai-api-user-quota-buckets.js +0 -54
  103. package/dist/server/collections/ai-api-user-quota-policies.js +0 -63
  104. package/dist/server/resource/ai-api-usage-groups.js +0 -168
  105. package/src/server/collections/ai-api-user-permissions.ts +0 -46
  106. package/src/server/collections/ai-api-user-quota-buckets.ts +0 -24
  107. package/src/server/collections/ai-api-user-quota-policies.ts +0 -33
  108. package/src/server/resource/ai-api-usage-groups.ts +0 -171
@@ -0,0 +1,530 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+
10
+ var __defProp = Object.defineProperty;
11
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
12
+ var __getOwnPropNames = Object.getOwnPropertyNames;
13
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
14
+ var __export = (target, all) => {
15
+ for (var name in all)
16
+ __defProp(target, name, { get: all[name], enumerable: true });
17
+ };
18
+ var __copyProps = (to, from, except, desc) => {
19
+ if (from && typeof from === "object" || typeof from === "function") {
20
+ for (let key of __getOwnPropNames(from))
21
+ if (!__hasOwnProp.call(to, key) && key !== except)
22
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
23
+ }
24
+ return to;
25
+ };
26
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
27
+ var responses_exports = {};
28
+ __export(responses_exports, {
29
+ RESPONSES_CAPABLE_PROVIDERS: () => RESPONSES_CAPABLE_PROVIDERS,
30
+ handleDeleteResponse: () => handleDeleteResponse,
31
+ handleGetResponse: () => handleGetResponse,
32
+ handleResponses: () => handleResponses
33
+ });
34
+ module.exports = __toCommonJS(responses_exports);
35
+ var import_billing = require("../billing");
36
+ var import_file_processor = require("../services/file-processor");
37
+ var import_usage = require("../usage");
38
+ var import_app_observability = require("../utils/app-observability");
39
+ var import_direct_llm_context = require("../utils/direct-llm-context");
40
+ var import_openai_format = require("../utils/openai-format");
41
+ var import_request_cache = require("../utils/request-cache");
42
+ var import_response_store = require("../utils/response-store");
43
+ var import_responses_format = require("../utils/responses-format");
44
+ var import_responses_stream = require("../utils/responses-stream");
45
+ var import_resolve_service = require("../utils/resolve-service");
46
+ var import_streaming = require("../utils/streaming");
47
+ var import_user_permissions = require("../utils/user-permissions");
48
+ var import_virtual_models = require("../utils/virtual-models");
49
+ var import_chat_completions = require("./chat-completions");
50
+ const RESPONSES_CAPABLE_PROVIDERS = /* @__PURE__ */ new Set(["openai"]);
51
+ function isResponsesCapableProvider(providerName) {
52
+ return RESPONSES_CAPABLE_PROVIDERS.has(providerName);
53
+ }
54
+ function getErrorMessage(error, fallback) {
55
+ return error instanceof Error && error.message ? error.message : fallback;
56
+ }
57
+ function getCurrentUserId(ctx) {
58
+ var _a;
59
+ return (_a = ctx.state.currentUser) == null ? void 0 : _a.id;
60
+ }
61
+ function validateRequest(ctx, body) {
62
+ if (typeof body.model !== "string" || !body.model) {
63
+ ctx.status = 400;
64
+ ctx.body = (0, import_openai_format.toOpenAIError)(400, "'model' is required", "invalid_request_error", "missing_model");
65
+ return false;
66
+ }
67
+ if (body.input === void 0 || body.input === null) {
68
+ ctx.status = 400;
69
+ ctx.body = (0, import_openai_format.toOpenAIError)(400, "'input' is required", "invalid_request_error", "missing_input");
70
+ return false;
71
+ }
72
+ if (body.model.trim().length === 0) {
73
+ ctx.status = 400;
74
+ ctx.body = (0, import_openai_format.toOpenAIError)(400, "'model' must not be blank", "invalid_request_error", "missing_model");
75
+ return false;
76
+ }
77
+ const parameterProblem = (0, import_responses_format.findResponseParameterProblem)(body);
78
+ if (parameterProblem) {
79
+ ctx.status = 400;
80
+ ctx.body = (0, import_openai_format.toOpenAIError)(400, parameterProblem, "invalid_request_error", "invalid_parameter");
81
+ return false;
82
+ }
83
+ const inputProblem = (0, import_responses_format.findResponseInputProblem)(body.input);
84
+ if (inputProblem) {
85
+ ctx.status = 400;
86
+ ctx.body = (0, import_openai_format.toOpenAIError)(400, inputProblem, "invalid_request_error", "invalid_input");
87
+ return false;
88
+ }
89
+ const unsupportedParameter = (0, import_responses_format.findUnsupportedResponseParameter)(body);
90
+ if (unsupportedParameter) {
91
+ ctx.status = 400;
92
+ ctx.body = (0, import_openai_format.toOpenAIError)(
93
+ 400,
94
+ `Responses API parameter '${unsupportedParameter}' is not supported by this gateway`,
95
+ "invalid_request_error",
96
+ "unsupported_parameter"
97
+ );
98
+ return false;
99
+ }
100
+ if (body.truncation !== void 0 && body.truncation !== null && body.truncation !== "auto" && body.truncation !== "disabled") {
101
+ ctx.status = 400;
102
+ ctx.body = (0, import_openai_format.toOpenAIError)(
103
+ 400,
104
+ "'truncation' must be 'auto' or 'disabled'",
105
+ "invalid_request_error",
106
+ "invalid_truncation"
107
+ );
108
+ return false;
109
+ }
110
+ if (body.service_tier !== void 0 && body.service_tier !== null && !(0, import_responses_format.isResponseServiceTier)(body.service_tier)) {
111
+ ctx.status = 400;
112
+ ctx.body = (0, import_openai_format.toOpenAIError)(
113
+ 400,
114
+ "'service_tier' must be one of auto, default, flex, scale, or priority",
115
+ "invalid_request_error",
116
+ "invalid_service_tier"
117
+ );
118
+ return false;
119
+ }
120
+ const unsupportedInput = (0, import_responses_format.findUnsupportedResponseInput)(body.input);
121
+ if (unsupportedInput) {
122
+ ctx.status = 400;
123
+ ctx.body = (0, import_openai_format.toOpenAIError)(
124
+ 400,
125
+ `Responses API input '${unsupportedInput}' is not supported because this gateway has no OpenAI file store. Use image_url, file_url, or file_data instead.`,
126
+ "invalid_request_error",
127
+ "unsupported_input"
128
+ );
129
+ return false;
130
+ }
131
+ const toolProblem = (0, import_responses_format.findResponseToolProblem)(body.tools);
132
+ if (toolProblem) {
133
+ ctx.status = 400;
134
+ ctx.body = (0, import_openai_format.toOpenAIError)(400, toolProblem, "invalid_request_error", "unsupported_tool");
135
+ return false;
136
+ }
137
+ const toolChoiceProblem = (0, import_responses_format.findResponseToolChoiceProblem)(body.tool_choice);
138
+ if (toolChoiceProblem) {
139
+ ctx.status = 400;
140
+ ctx.body = (0, import_openai_format.toOpenAIError)(400, toolChoiceProblem, "invalid_request_error", "invalid_tool_choice");
141
+ return false;
142
+ }
143
+ if (body.metadata !== void 0 && body.metadata !== null) {
144
+ const entries = (0, import_responses_format.isRecord)(body.metadata) ? Object.entries(body.metadata) : [];
145
+ if (!(0, import_responses_format.isRecord)(body.metadata) || entries.length > 16 || entries.some(([key, value]) => key.length > 64 || typeof value !== "string" || value.length > 512)) {
146
+ ctx.status = 400;
147
+ ctx.body = (0, import_openai_format.toOpenAIError)(
148
+ 400,
149
+ "'metadata' must contain at most 16 string values (keys <= 64 chars, values <= 512 chars)",
150
+ "invalid_request_error",
151
+ "invalid_metadata"
152
+ );
153
+ return false;
154
+ }
155
+ }
156
+ return true;
157
+ }
158
+ function toLangChainMessages(messages) {
159
+ return messages.map((message) => {
160
+ const role = message.role === "assistant" ? "ai" : message.role;
161
+ const content = (0, import_chat_completions.normalizeMessageContent)(message.content);
162
+ if (message.role === "assistant" && message.tool_calls) {
163
+ return {
164
+ role,
165
+ content,
166
+ tool_calls: message.tool_calls,
167
+ additional_kwargs: {
168
+ ...(0, import_responses_format.isRecord)(message.additional_kwargs) ? message.additional_kwargs : {},
169
+ tool_calls: message.tool_calls
170
+ },
171
+ ...(0, import_responses_format.isRecord)(message.response_metadata) ? { response_metadata: message.response_metadata } : {}
172
+ };
173
+ }
174
+ if (message.role === "assistant" && message.additional_kwargs) {
175
+ return {
176
+ role,
177
+ content,
178
+ additional_kwargs: message.additional_kwargs,
179
+ ...(0, import_responses_format.isRecord)(message.response_metadata) ? { response_metadata: message.response_metadata } : {}
180
+ };
181
+ }
182
+ if (message.role === "tool") {
183
+ return { role: "tool", content, tool_call_id: message.tool_call_id, name: message.name };
184
+ }
185
+ return [role, content];
186
+ });
187
+ }
188
+ function responseOverflowBehavior(body) {
189
+ return body.truncation === "auto" ? "truncate" : "reject";
190
+ }
191
+ const RESPONSE_INVOCATION_PARAMETERS = /* @__PURE__ */ new Set([
192
+ "max_completion_tokens",
193
+ "parallel_tool_calls",
194
+ "promptCacheKey",
195
+ "promptCacheRetention",
196
+ "reasoning",
197
+ "response_format",
198
+ "service_tier",
199
+ "temperature",
200
+ "top_p",
201
+ "user",
202
+ "verbosity"
203
+ ]);
204
+ function responseModelKwargs(parameters) {
205
+ return Object.fromEntries(Object.entries(parameters).filter(([name]) => !RESPONSE_INVOCATION_PARAMETERS.has(name)));
206
+ }
207
+ function findCompletedResponse(events) {
208
+ const terminal = events.find((event) => event.type === "response.completed" || event.type === "response.incomplete");
209
+ return terminal && (0, import_responses_format.isRecord)(terminal.response) ? terminal.response : void 0;
210
+ }
211
+ function findUnsupportedRetrieveParameter(ctx) {
212
+ const query = (0, import_responses_format.isRecord)(ctx.query) ? ctx.query : {};
213
+ for (const name of ["include", "include[]", "include_obfuscation", "starting_after"]) {
214
+ if (Object.hasOwn(query, name)) return name === "include[]" ? "include" : name;
215
+ }
216
+ if (Object.hasOwn(query, "stream") && query.stream !== false && query.stream !== "false") return "stream";
217
+ return void 0;
218
+ }
219
+ async function handleGetResponse(ctx, responseId) {
220
+ const unsupportedParameter = findUnsupportedRetrieveParameter(ctx);
221
+ if (unsupportedParameter) {
222
+ ctx.status = 400;
223
+ ctx.body = (0, import_openai_format.toOpenAIError)(
224
+ 400,
225
+ `Responses API retrieve parameter '${unsupportedParameter}' is not supported by this gateway`,
226
+ "invalid_request_error",
227
+ "unsupported_parameter"
228
+ );
229
+ return;
230
+ }
231
+ const userId = getCurrentUserId(ctx);
232
+ const record = userId === void 0 ? null : await (0, import_response_store.getResponseRecord)(ctx, responseId, userId);
233
+ if (!record) {
234
+ ctx.status = 404;
235
+ ctx.body = (0, import_openai_format.toOpenAIError)(
236
+ 404,
237
+ `Response '${responseId}' was not found`,
238
+ "invalid_request_error",
239
+ "response_not_found"
240
+ );
241
+ return;
242
+ }
243
+ ctx.status = 200;
244
+ ctx.body = record.output;
245
+ }
246
+ async function handleDeleteResponse(ctx, responseId) {
247
+ const userId = getCurrentUserId(ctx);
248
+ const deleted = userId === void 0 ? false : await (0, import_response_store.deleteResponseRecord)(ctx, responseId, userId);
249
+ if (!deleted) {
250
+ ctx.status = 404;
251
+ ctx.body = (0, import_openai_format.toOpenAIError)(
252
+ 404,
253
+ `Response '${responseId}' was not found`,
254
+ "invalid_request_error",
255
+ "response_not_found"
256
+ );
257
+ return;
258
+ }
259
+ ctx.status = 204;
260
+ ctx.body = void 0;
261
+ }
262
+ async function handleResponses(ctx, plugin) {
263
+ var _a, _b;
264
+ const body = ctx.request.body ?? {};
265
+ if (!validateRequest(ctx, body)) return;
266
+ const chatBody = (0, import_responses_format.responsesBodyToChatBody)(body);
267
+ let messages = Array.isArray(chatBody.messages) ? chatBody.messages : [];
268
+ if (messages.length === 0) {
269
+ ctx.status = 400;
270
+ ctx.body = (0, import_openai_format.toOpenAIError)(
271
+ 400,
272
+ "'input' must contain at least one supported message or function call item",
273
+ "invalid_request_error",
274
+ "invalid_input"
275
+ );
276
+ return;
277
+ }
278
+ const messageProblem = (0, import_chat_completions.findMessageProblem)(messages);
279
+ if (messageProblem) {
280
+ ctx.status = 400;
281
+ ctx.body = (0, import_openai_format.toOpenAIError)(
282
+ 400,
283
+ `Invalid input[${messageProblem.index}]: ${messageProblem.reason}.`,
284
+ "invalid_request_error",
285
+ "invalid_input"
286
+ );
287
+ return;
288
+ }
289
+ const blockProblem = (0, import_chat_completions.findContentBlockProblem)(messages);
290
+ if (blockProblem) {
291
+ ctx.status = 400;
292
+ ctx.body = (0, import_openai_format.toOpenAIError)(
293
+ 400,
294
+ `Invalid content block in input[${blockProblem.index}]: ${blockProblem.reason}.`,
295
+ "invalid_request_error",
296
+ "invalid_content_block"
297
+ );
298
+ return;
299
+ }
300
+ const userId = getCurrentUserId(ctx);
301
+ if (typeof body.previous_response_id === "string") {
302
+ if (userId === void 0) {
303
+ ctx.status = 401;
304
+ ctx.body = (0, import_openai_format.toOpenAIError)(401, "Authentication is required", "authentication_error", "invalid_api_key");
305
+ return;
306
+ }
307
+ const previousMessages = await (0, import_response_store.loadConversationChain)(ctx, body.previous_response_id, userId);
308
+ if (!previousMessages) {
309
+ ctx.status = 404;
310
+ ctx.body = (0, import_openai_format.toOpenAIError)(
311
+ 404,
312
+ `Previous response '${body.previous_response_id}' was not found`,
313
+ "invalid_request_error",
314
+ "previous_response_not_found"
315
+ );
316
+ return;
317
+ }
318
+ messages = [...previousMessages, ...messages];
319
+ }
320
+ const virtual = await (0, import_virtual_models.resolveVirtualModel)(ctx, body.model, chatBody, "chat");
321
+ if ((virtual == null ? void 0 : virtual.status) === "unavailable") {
322
+ (0, import_virtual_models.respondVirtualModelUnavailable)(ctx, virtual);
323
+ return;
324
+ }
325
+ if ((virtual == null ? void 0 : virtual.status) === "resolved") {
326
+ ctx.state.aiApiVirtualModel = virtual.virtualModel;
327
+ ctx.state.aiApiRoutingReason = virtual.reason;
328
+ }
329
+ const resolved = (virtual == null ? void 0 : virtual.resolved) ?? await (0, import_resolve_service.resolveModelString)(ctx, body.model);
330
+ if (!resolved) {
331
+ ctx.status = 404;
332
+ ctx.body = (0, import_openai_format.toOpenAIError)(
333
+ 404,
334
+ `Could not resolve model '${body.model}'. Format: 'serviceName/modelId'. Use GET /v1/models to see available models.`,
335
+ "invalid_request_error",
336
+ "model_not_found"
337
+ );
338
+ return;
339
+ }
340
+ const { service, modelId } = resolved;
341
+ try {
342
+ const aiPlugin = ctx.app.pm.get("ai");
343
+ if (!((_a = aiPlugin == null ? void 0 : aiPlugin.aiManager) == null ? void 0 : _a.llmProviders)) {
344
+ ctx.status = 500;
345
+ ctx.body = (0, import_openai_format.toOpenAIError)(500, "AI plugin not available", "server_error");
346
+ return;
347
+ }
348
+ if (service.get("enabled") === false) {
349
+ ctx.status = 404;
350
+ ctx.body = (0, import_openai_format.toOpenAIError)(404, "LLM service is disabled", "invalid_request_error", "model_not_found");
351
+ return;
352
+ }
353
+ const config = await (0, import_request_cache.getAiApiConfig)(ctx);
354
+ if (!await (0, import_user_permissions.enforceModelAccess)(ctx, config == null ? void 0 : config.enabledLlmServices, service, modelId)) return;
355
+ const providerName = service.get("provider");
356
+ const providerMeta = aiPlugin.aiManager.llmProviders.get(providerName);
357
+ if (!providerMeta) {
358
+ ctx.status = 500;
359
+ ctx.body = (0, import_openai_format.toOpenAIError)(500, `Provider '${providerName}' not registered`, "server_error");
360
+ return;
361
+ }
362
+ messages = await Promise.all(
363
+ messages.map(async (message) => ({
364
+ ...message,
365
+ content: await (0, import_chat_completions.processMessageContentFileBlocks)(message.content, ctx, plugin)
366
+ }))
367
+ );
368
+ const prepared = await (0, import_direct_llm_context.prepareDirectLlmContext)(ctx, {
369
+ serviceName: service.get("name"),
370
+ modelId,
371
+ messages,
372
+ tools: chatBody.tools,
373
+ maxCompletionTokens: chatBody.max_completion_tokens,
374
+ overflowBehavior: responseOverflowBehavior(body)
375
+ });
376
+ await (0, import_billing.prepareLlmBilling)(ctx, resolved);
377
+ const modelOptions = {
378
+ model: modelId,
379
+ llmService: service.get("name")
380
+ };
381
+ if (chatBody.temperature !== void 0) modelOptions.temperature = chatBody.temperature;
382
+ if (chatBody.top_p !== void 0) modelOptions.topP = chatBody.top_p;
383
+ if (chatBody.max_completion_tokens !== void 0) modelOptions.maxTokens = chatBody.max_completion_tokens;
384
+ if (chatBody.reasoning !== void 0) modelOptions.reasoning = chatBody.reasoning;
385
+ if (chatBody.service_tier !== void 0) modelOptions.service_tier = chatBody.service_tier;
386
+ if (chatBody.promptCacheKey !== void 0) modelOptions.promptCacheKey = chatBody.promptCacheKey;
387
+ if (chatBody.promptCacheRetention !== void 0) modelOptions.promptCacheRetention = chatBody.promptCacheRetention;
388
+ if (chatBody.verbosity !== void 0) modelOptions.verbosity = chatBody.verbosity;
389
+ if (chatBody.user !== void 0) modelOptions.user = chatBody.user;
390
+ const provider = new providerMeta.provider({
391
+ app: ctx.app,
392
+ serviceOptions: service.get("options"),
393
+ modelOptions
394
+ });
395
+ const providerParameters = (0, import_chat_completions.getProviderRequestParameters)(chatBody);
396
+ const baseModel = provider.createModel();
397
+ (0, import_chat_completions.applyProviderRequestParameters)(baseModel, responseModelKwargs(providerParameters));
398
+ if (body.store === false && isResponsesCapableProvider(providerName) && baseModel && typeof baseModel === "object") {
399
+ baseModel.modelKwargs = {
400
+ ...baseModel.modelKwargs ?? {},
401
+ store: false
402
+ };
403
+ }
404
+ const chatModel = (0, import_chat_completions.bindRequestTools)(
405
+ baseModel,
406
+ chatBody.tools,
407
+ chatBody.tool_choice,
408
+ providerParameters
409
+ );
410
+ (0, import_billing.markLlmProviderAttempted)(ctx);
411
+ const modelName = `${service.get("name")}/${modelId}`;
412
+ const langchainMessages = toLangChainMessages(prepared.messages);
413
+ if (body.stream === true) {
414
+ await handleStreamingResponse(ctx, chatModel, langchainMessages, body, modelName, providerParameters, userId);
415
+ } else {
416
+ await handleNonStreamingResponse(ctx, chatModel, langchainMessages, body, modelName, providerParameters, userId);
417
+ }
418
+ } catch (error) {
419
+ ctx.log.error("AI API responses error:", error);
420
+ if (!((_b = ctx.res) == null ? void 0 : _b.headersSent)) {
421
+ const quotaError = error instanceof import_billing.AiApiQuotaError;
422
+ const requestError = error instanceof import_direct_llm_context.DirectLlmContextError || error instanceof import_file_processor.FileProcessorError;
423
+ ctx.status = quotaError ? 429 : requestError ? 400 : 500;
424
+ if (quotaError) ctx.set("X-RateLimit-Reason", error.code);
425
+ ctx.body = (0, import_openai_format.toOpenAIError)(
426
+ ctx.status,
427
+ getErrorMessage(error, "Internal server error"),
428
+ quotaError ? "quota_error" : requestError ? "invalid_request_error" : "server_error",
429
+ quotaError || requestError ? error.code : void 0
430
+ );
431
+ }
432
+ }
433
+ }
434
+ async function handleNonStreamingResponse(ctx, chatModel, messages, body, modelName, providerParameters, userId) {
435
+ const result = await chatModel.invoke(messages, providerParameters);
436
+ const content = (0, import_responses_format.extractResponseOutputText)(result);
437
+ const responseId = (0, import_responses_format.generateResponseId)();
438
+ const usage = (0, import_usage.setAiApiUsageResult)(
439
+ ctx,
440
+ result.usage_metadata,
441
+ { gatewayResponseId: responseId, providerRequestId: (0, import_usage.extractProviderRequestId)(result) },
442
+ result.response_metadata
443
+ );
444
+ const response = (0, import_responses_format.chatResultToResponse)({
445
+ id: responseId,
446
+ model: modelName,
447
+ content,
448
+ reasoningText: (0, import_responses_format.extractReasoningText)(result),
449
+ usage,
450
+ toolCalls: (0, import_chat_completions.normalizeToolCalls)(result.tool_calls),
451
+ finishReason: (0, import_chat_completions.extractFinishReason)(result) ?? (0, import_responses_format.extractResponseIncompleteReason)(result),
452
+ serviceTier: (0, import_responses_format.extractResponseServiceTier)(result),
453
+ requestBody: body
454
+ });
455
+ if (body.store !== false && userId !== void 0) await (0, import_response_store.storeResponseRecord)(ctx, response, body, userId);
456
+ ctx.status = 200;
457
+ ctx.body = response;
458
+ }
459
+ async function handleStreamingResponse(ctx, chatModel, messages, body, modelName, providerParameters, userId) {
460
+ const responseId = (0, import_responses_format.generateResponseId)();
461
+ const state = (0, import_responses_stream.createResponseStreamState)(responseId, modelName, body);
462
+ ctx.set({
463
+ "Content-Type": "text/event-stream",
464
+ "Cache-Control": "no-cache",
465
+ Connection: "keep-alive",
466
+ "X-Accel-Buffering": "no"
467
+ });
468
+ ctx.status = 200;
469
+ for (const event of (0, import_responses_stream.createResponseStartEvents)(state)) await (0, import_streaming.writeResponse)(ctx, (0, import_openai_format.formatSSE)(event));
470
+ const requestAbort = (0, import_streaming.createRequestAbortController)(ctx);
471
+ let usage;
472
+ let usageMetadata;
473
+ let providerRequestId;
474
+ try {
475
+ const stream = await chatModel.stream(messages, { ...providerParameters, signal: requestAbort.signal });
476
+ for await (const chunk of stream) {
477
+ if (requestAbort.signal.aborted) throw requestAbort.signal.reason;
478
+ const events = (0, import_responses_stream.appendResponseStreamChunk)(state, chunk);
479
+ if (events.length) (0, import_app_observability.markAiApiFirstProviderOutput)(ctx);
480
+ for (const event of events) await (0, import_streaming.writeResponse)(ctx, (0, import_openai_format.formatSSE)(event));
481
+ const normalized = (0, import_usage.normalizeUsage)(chunk.usage_metadata);
482
+ if (normalized) {
483
+ usage = normalized;
484
+ usageMetadata = chunk.response_metadata;
485
+ (0, import_responses_stream.setResponseStreamUsage)(state, normalized);
486
+ }
487
+ providerRequestId = providerRequestId ?? (0, import_usage.extractProviderRequestId)(chunk);
488
+ }
489
+ const finalEvents = (0, import_responses_stream.finalizeResponseStream)(state);
490
+ const response = findCompletedResponse(finalEvents);
491
+ if (response && body.store !== false && userId !== void 0) {
492
+ try {
493
+ await (0, import_response_store.storeResponseRecord)(ctx, response, body, userId);
494
+ } catch (storeError) {
495
+ ctx.log.error("[ai-api] Failed to persist streamed response record:", storeError);
496
+ }
497
+ }
498
+ for (const event of finalEvents) await (0, import_streaming.writeResponse)(ctx, (0, import_openai_format.formatSSE)(event));
499
+ await (0, import_streaming.writeResponse)(ctx, (0, import_openai_format.formatSSEDone)());
500
+ (0, import_usage.setAiApiUsageResult)(ctx, usage, { gatewayResponseId: responseId, providerRequestId }, usageMetadata);
501
+ ctx.state.aiApiStreamResult = { succeeded: true, id: responseId };
502
+ } catch (error) {
503
+ const cancelled = (0, import_streaming.isClientDisconnected)(ctx, error);
504
+ ctx.log.error("AI API Responses streaming error:", error);
505
+ if (!ctx.res.destroyed && !ctx.res.writableEnded) {
506
+ const streamError = {
507
+ message: getErrorMessage(error, "Streaming error"),
508
+ code: cancelled ? "client_disconnected" : "server_error"
509
+ };
510
+ await (0, import_streaming.writeResponse)(ctx, (0, import_openai_format.formatSSE)((0, import_responses_stream.createResponseErrorEvent)(state, streamError)));
511
+ await (0, import_streaming.writeResponse)(ctx, (0, import_openai_format.formatSSE)((0, import_responses_stream.createResponseFailedEvent)(state, streamError)));
512
+ }
513
+ (0, import_usage.setAiApiUsageResult)(ctx, usage, { gatewayResponseId: responseId, providerRequestId }, usageMetadata);
514
+ ctx.state.aiApiStreamResult = {
515
+ succeeded: false,
516
+ id: responseId,
517
+ errorCode: cancelled ? "client_disconnected" : "stream_error"
518
+ };
519
+ } finally {
520
+ requestAbort.dispose();
521
+ if (!ctx.res.writableEnded && !ctx.res.destroyed) ctx.res.end();
522
+ }
523
+ }
524
+ // Annotate the CommonJS export names for ESM import in node:
525
+ 0 && (module.exports = {
526
+ RESPONSES_CAPABLE_PROVIDERS,
527
+ handleDeleteResponse,
528
+ handleGetResponse,
529
+ handleResponses
530
+ });
@@ -50,6 +50,7 @@ var import_chat_completions = require("./chat-completions");
50
50
  var import_completions = require("./completions");
51
51
  var import_agent_completions = require("./agent-completions");
52
52
  var import_embeddings = require("./embeddings");
53
+ var import_responses = require("./responses");
53
54
  var import_openai_format = require("../utils/openai-format");
54
55
  var import_rate_limit = require("../middleware/rate-limit");
55
56
  var import_role_permission = require("../middleware/role-permission");
@@ -63,7 +64,7 @@ const AI_LLM_PREFIX = API_PREFIX;
63
64
  function createAiLlmRouter(plugin) {
64
65
  const checkRateLimit = (0, import_rate_limit.createRateLimitMiddleware)(plugin.rateLimiter);
65
66
  return async (ctx, next) => {
66
- var _a, _b;
67
+ var _a, _b, _c;
67
68
  const { path, method } = ctx;
68
69
  if (!path.startsWith(API_PREFIX)) {
69
70
  return next();
@@ -124,11 +125,11 @@ function createAiLlmRouter(plugin) {
124
125
  }
125
126
  const requestBody = ctx.request.body || {};
126
127
  const model = requestBody.model === void 0 || requestBody.model === null ? "-" : String(requestBody.model);
127
- const isUsageEndpoint = method === "POST" && (subPath === "/chat/completions" || subPath === "/completions" || subPath === "/embeddings");
128
- const isStreamingEndpoint = method === "POST" && (subPath === "/chat/completions" || subPath === "/completions");
129
- const resolvedMode = isUsageEndpoint ? await resolveMode(ctx) : "llm";
130
- const streaming = isStreamingEndpoint && (0, import_streaming.isStreamingRequested)(requestBody.stream);
131
- if (streaming) {
128
+ const isUsageEndpoint = method === "POST" && (subPath === "/chat/completions" || subPath === "/completions" || subPath === "/embeddings" || subPath === "/responses");
129
+ const isStreamingEndpoint = method === "POST" && (subPath === "/chat/completions" || subPath === "/completions" || subPath === "/responses");
130
+ const resolvedMode = subPath === "/responses" ? "llm" : isUsageEndpoint ? await resolveMode(ctx) : "llm";
131
+ const streaming = isStreamingEndpoint && (subPath === "/responses" ? requestBody.stream === true : (0, import_streaming.isStreamingRequested)(requestBody.stream));
132
+ if (streaming && subPath !== "/responses") {
132
133
  const streamOptions = requestBody.stream_options;
133
134
  ctx.request.body = {
134
135
  ...requestBody,
@@ -140,7 +141,7 @@ function createAiLlmRouter(plugin) {
140
141
  }
141
142
  const t0 = Date.now();
142
143
  if (isUsageEndpoint) {
143
- const service = subPath === "/embeddings" ? "llm.embedding" : resolvedMode === "agent" ? "llm.agent" : subPath === "/completions" ? "llm.completion" : "llm.chat";
144
+ const service = subPath === "/embeddings" ? "llm.embedding" : subPath === "/responses" ? "llm.responses" : resolvedMode === "agent" ? "llm.agent" : subPath === "/completions" ? "llm.completion" : "llm.chat";
144
145
  (0, import_app_observability.startAiApiObservation)(ctx, {
145
146
  service,
146
147
  operation: subPath,
@@ -172,6 +173,51 @@ function createAiLlmRouter(plugin) {
172
173
  logRequest(ctx, requestId, model, "ok", Date.now() - t0);
173
174
  return;
174
175
  }
176
+ if (method === "POST" && subPath === "/responses") {
177
+ await (0, import_responses.handleResponses)(ctx, plugin);
178
+ logRequest(
179
+ ctx,
180
+ requestId,
181
+ model,
182
+ ((_b = ctx.state.aiApiStreamResult) == null ? void 0 : _b.succeeded) === false ? "error" : "ok",
183
+ Date.now() - t0
184
+ );
185
+ return;
186
+ }
187
+ if ((method === "GET" || method === "DELETE") && subPath.startsWith("/responses/")) {
188
+ const responseId = subPath.substring("/responses/".length);
189
+ if (responseId) {
190
+ let decodedResponseId;
191
+ try {
192
+ decodedResponseId = decodeURIComponent(responseId);
193
+ } catch {
194
+ ctx.status = 400;
195
+ ctx.body = (0, import_openai_format.toOpenAIError)(
196
+ 400,
197
+ "Response ID contains invalid URL encoding",
198
+ "invalid_request_error",
199
+ "invalid_response_id"
200
+ );
201
+ logRequest(ctx, requestId, "-", "invalid_response_id", Date.now() - t0);
202
+ return;
203
+ }
204
+ if (decodedResponseId.includes("/")) {
205
+ ctx.status = 404;
206
+ ctx.body = (0, import_openai_format.toOpenAIError)(
207
+ 404,
208
+ `Unknown endpoint: ${method} ${path}`,
209
+ "invalid_request_error",
210
+ "unknown_url"
211
+ );
212
+ logRequest(ctx, requestId, "-", "not_found", Date.now() - t0);
213
+ return;
214
+ }
215
+ if (method === "GET") await (0, import_responses.handleGetResponse)(ctx, decodedResponseId);
216
+ else await (0, import_responses.handleDeleteResponse)(ctx, decodedResponseId);
217
+ logRequest(ctx, requestId, "-", ctx.status === 404 ? "not_found" : "ok", Date.now() - t0);
218
+ return;
219
+ }
220
+ }
175
221
  if (method === "POST" && subPath === "/completions") {
176
222
  const completionsMode = resolvedMode;
177
223
  if (completionsMode === "agent") {
@@ -188,7 +234,7 @@ function createAiLlmRouter(plugin) {
188
234
  ctx,
189
235
  requestId,
190
236
  model,
191
- ((_b = ctx.state.aiApiStreamResult) == null ? void 0 : _b.succeeded) === false ? "error" : "ok",
237
+ ((_c = ctx.state.aiApiStreamResult) == null ? void 0 : _c.succeeded) === false ? "error" : "ok",
192
238
  Date.now() - t0
193
239
  );
194
240
  return;
@@ -201,7 +247,16 @@ function createAiLlmRouter(plugin) {
201
247
  if (method === "GET" && subPath.startsWith("/models/")) {
202
248
  const modelId = subPath.substring("/models/".length);
203
249
  if (modelId) {
204
- await (0, import_models.handleGetModel)(ctx, decodeURIComponent(modelId), plugin);
250
+ let decodedModelId;
251
+ try {
252
+ decodedModelId = decodeURIComponent(modelId);
253
+ } catch {
254
+ ctx.status = 400;
255
+ ctx.body = (0, import_openai_format.toOpenAIError)(400, "Model ID contains invalid URL encoding", "invalid_request_error");
256
+ logRequest(ctx, requestId, modelId, "bad_request", Date.now() - t0);
257
+ return;
258
+ }
259
+ await (0, import_models.handleGetModel)(ctx, decodedModelId, plugin);
205
260
  logRequest(ctx, requestId, modelId, "ok", Date.now() - t0);
206
261
  return;
207
262
  }
@@ -220,7 +275,7 @@ function createAiLlmRouter(plugin) {
220
275
  ctx.status = 404;
221
276
  ctx.body = (0, import_openai_format.toOpenAIError)(
222
277
  404,
223
- `Unknown endpoint: ${method} ${path}. Supported: POST /v1/chat/completions, POST /v1/completions, POST /v1/embeddings, GET /v1/models`,
278
+ `Unknown endpoint: ${method} ${path}. Supported: POST /v1/chat/completions, POST /v1/completions, POST /v1/embeddings, POST /v1/responses, GET/DELETE /v1/responses/:id, GET /v1/models`,
224
279
  "invalid_request_error",
225
280
  "unknown_url"
226
281
  );