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.
- package/README.md +51 -12
- package/dist/client/185.c47663fefaeb0e5b.js +10 -0
- package/dist/client/562.9012cfd1fa04303d.js +10 -0
- package/dist/client/685.b5b1e0a5b825d253.js +10 -0
- package/dist/client/index.js +1 -1
- package/dist/client-v2/185.b552dc91ec2371ba.js +10 -0
- package/dist/client-v2/562.db2984167250b1be.js +10 -0
- package/dist/client-v2/685.cf16e5b829e06f85.js +10 -0
- package/dist/client-v2/index.js +1 -1
- package/dist/externalVersion.js +8 -8
- package/dist/locale/en-US.json +175 -139
- package/dist/locale/vi-VN.json +40 -2
- package/dist/locale/zh-CN.json +40 -2
- package/dist/server/collections/ai-api-model-metadata.js +26 -0
- package/dist/server/collections/ai-api-response-records.js +101 -0
- package/dist/server/collections/ai-api-virtual-models.js +68 -0
- package/dist/server/middleware/response-record-resource.js +66 -0
- package/dist/server/middleware/role-permission.js +43 -18
- package/dist/server/migrations/20260901000000-remove-default-group-members.js +60 -0
- package/dist/server/migrations/20260902000000-seed-default-role-permissions.js +55 -0
- package/dist/server/migrations/20260903000000-seed-sample-response-records.js +170 -0
- package/dist/server/plugin.js +66 -16
- package/dist/server/routes/chat-completions.js +38 -6
- package/dist/server/routes/completions.js +16 -4
- package/dist/server/routes/embeddings.js +25 -6
- package/dist/server/routes/models.js +29 -0
- package/dist/server/routes/responses.js +530 -0
- package/dist/server/routes/router.js +65 -10
- package/dist/server/usage.js +25 -4
- package/dist/server/utils/direct-llm-context.js +1 -1
- package/dist/server/utils/resolve-service.js +24 -0
- package/dist/server/utils/response-store.js +138 -0
- package/dist/server/utils/responses-format.js +686 -0
- package/dist/server/utils/responses-stream.js +330 -0
- package/dist/server/utils/virtual-models.js +238 -0
- package/dist/server/validation.js +44 -2
- package/dist/swagger.js +137 -0
- package/package.json +34 -32
- package/src/__tests__/locale.test.ts +43 -0
- package/src/client/__tests__/settings-registration.test.tsx +1 -0
- package/src/client/plugin.tsx +9 -1
- package/src/client-v2/__tests__/settings-registration.test.tsx +1 -0
- package/src/client-v2/pages/ModelMetadataPage.tsx +44 -0
- package/src/client-v2/pages/ModelRoutingPage.tsx +238 -0
- package/src/client-v2/pages/UsageGroupsPage.tsx +75 -38
- package/src/client-v2/plugin.tsx +8 -0
- package/src/locale/en-US.json +175 -139
- package/src/locale/vi-VN.json +40 -2
- package/src/locale/zh-CN.json +40 -2
- package/src/server/__tests__/embeddings.test.ts +184 -0
- package/src/server/__tests__/models.test.ts +21 -1
- package/src/server/__tests__/response-record-resource.test.ts +50 -0
- package/src/server/__tests__/response-store-integration.test.ts +341 -0
- package/src/server/__tests__/response-store.test.ts +195 -0
- package/src/server/__tests__/responses-contract.test.ts +469 -0
- package/src/server/__tests__/responses-format.test.ts +299 -0
- package/src/server/__tests__/responses-router.test.ts +182 -0
- package/src/server/__tests__/responses-streaming.test.ts +368 -0
- package/src/server/__tests__/responses.test.ts +462 -0
- package/src/server/__tests__/role-permission.test.ts +139 -0
- package/src/server/__tests__/seed-role-permission.test.ts +88 -0
- package/src/server/__tests__/types/responses-sdk.types.test-d.ts +23 -0
- package/src/server/__tests__/usage-groups.test.ts +96 -0
- package/src/server/__tests__/usage-route.test.ts +1 -0
- package/src/server/__tests__/usage.test.ts +14 -0
- package/src/server/__tests__/validation.test.ts +66 -7
- package/src/server/__tests__/virtual-model-routing.test.ts +589 -0
- package/src/server/collections/ai-api-model-metadata.ts +26 -0
- package/src/server/collections/ai-api-response-records.ts +77 -0
- package/src/server/collections/ai-api-virtual-models.ts +58 -0
- package/src/server/middleware/response-record-resource.ts +44 -0
- package/src/server/middleware/role-permission.ts +69 -35
- package/src/server/migrations/20260901000000-remove-default-group-members.ts +56 -0
- package/src/server/migrations/20260902000000-seed-default-role-permissions.ts +46 -0
- package/src/server/migrations/20260903000000-seed-sample-response-records.ts +162 -0
- package/src/server/plugin.ts +84 -20
- package/src/server/resource/ai-api-config.ts +2 -1
- package/src/server/routes/agent-completions.ts +3 -0
- package/src/server/routes/chat-completions.ts +34 -10
- package/src/server/routes/completions.ts +16 -4
- package/src/server/routes/embeddings.ts +32 -10
- package/src/server/routes/models.ts +34 -0
- package/src/server/routes/responses.ts +640 -0
- package/src/server/routes/router.ts +81 -12
- package/src/server/services/__tests__/file-processor.test.ts +1 -0
- package/src/server/usage.ts +29 -2
- package/src/server/utils/app-observability.ts +1 -1
- package/src/server/utils/direct-llm-context.ts +2 -1
- package/src/server/utils/openai-format.ts +1 -0
- package/src/server/utils/resolve-service.ts +39 -1
- package/src/server/utils/response-store.ts +148 -0
- package/src/server/utils/responses-format.ts +974 -0
- package/src/server/utils/responses-stream.ts +384 -0
- package/src/server/utils/virtual-models.ts +320 -0
- package/src/server/validation.ts +49 -0
- package/src/swagger.ts +139 -0
- package/dist/client/562.44b16aad4718b4c7.js +0 -10
- package/dist/client/685.ae483e17b6b49c98.js +0 -10
- package/dist/client-v2/562.45d5c504433be38b.js +0 -10
- package/dist/client-v2/685.1030370b309b7d4b.js +0 -10
- package/dist/server/collections/ai-api-user-permissions.js +0 -67
- package/dist/server/collections/ai-api-user-quota-buckets.js +0 -54
- package/dist/server/collections/ai-api-user-quota-policies.js +0 -63
- package/dist/server/resource/ai-api-usage-groups.js +0 -168
- package/src/server/collections/ai-api-user-permissions.ts +0 -46
- package/src/server/collections/ai-api-user-quota-buckets.ts +0 -24
- package/src/server/collections/ai-api-user-quota-policies.ts +0 -33
- 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
|
-
((
|
|
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
|
-
|
|
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
|
);
|