waypoi 0.0.0
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/.github/instructions/ui.instructions.md +42 -0
- package/.github/workflows/ci.yml +35 -0
- package/.github/workflows/publish.yml +71 -0
- package/.github/workflows/release.yml +48 -0
- package/.playwright-mcp/console-2026-04-04T01-41-10-746Z.log +2 -0
- package/.playwright-mcp/console-2026-04-04T01-41-28-799Z.log +3 -0
- package/.playwright-mcp/console-2026-04-05T02-26-51-909Z.log +76 -0
- package/.playwright-mcp/page-2026-04-04T01-41-10-816Z.yml +1 -0
- package/.playwright-mcp/page-2026-04-04T01-41-29-141Z.yml +77 -0
- package/.playwright-mcp/page-2026-04-04T01-41-42-633Z.yml +190 -0
- package/.playwright-mcp/page-2026-04-04T01-42-03-929Z.yml +262 -0
- package/.playwright-mcp/page-2026-04-04T02-12-54-813Z.yml +6 -0
- package/.playwright-mcp/page-2026-04-04T02-14-58-600Z.yml +190 -0
- package/.playwright-mcp/page-2026-04-04T02-15-03-923Z.yml +190 -0
- package/.playwright-mcp/page-2026-04-04T02-15-07-426Z.yml +190 -0
- package/.playwright-mcp/page-2026-04-04T02-15-25-729Z.yml +262 -0
- package/.playwright-mcp/page-2026-04-04T02-16-22-984Z.yml +262 -0
- package/.playwright-mcp/page-2026-04-04T02-17-00-599Z.yml +190 -0
- package/.playwright-mcp/page-2026-04-04T02-17-50-874Z.yml +190 -0
- package/.playwright-mcp/page-2026-04-05T02-26-55-570Z.yml +6 -0
- package/AGENTS.md +48 -0
- package/CHANGELOG.md +131 -0
- package/README.md +552 -0
- package/assets/agent-mode.png +0 -0
- package/assets/categorize.png +0 -0
- package/assets/dashboard.png +0 -0
- package/assets/endpoint-proxy.png +0 -0
- package/assets/icon.png +0 -0
- package/assets/mcp-generate-image.png +0 -0
- package/assets/mcp-understand-image.png +0 -0
- package/assets/peek-token-flow.png +0 -0
- package/assets/playground.png +0 -0
- package/assets/sankey.png +0 -0
- package/cli/index.ts +2805 -0
- package/cli/legacyRewrite.ts +108 -0
- package/cli/modelRef.ts +24 -0
- package/dist/cli/index.js +2536 -0
- package/dist/cli/legacyRewrite.js +92 -0
- package/dist/cli/modelRef.js +20 -0
- package/dist/src/benchmark/artifacts.js +131 -0
- package/dist/src/benchmark/capabilityClassifier.js +81 -0
- package/dist/src/benchmark/capabilityStore.js +144 -0
- package/dist/src/benchmark/config.js +238 -0
- package/dist/src/benchmark/gates.js +118 -0
- package/dist/src/benchmark/jobs.js +252 -0
- package/dist/src/benchmark/runner.js +1847 -0
- package/dist/src/benchmark/schema.js +353 -0
- package/dist/src/benchmark/suites.js +314 -0
- package/dist/src/benchmark/tinyQaDataset.js +422 -0
- package/dist/src/benchmark/types.js +25 -0
- package/dist/src/config.js +47 -0
- package/dist/src/index.js +178 -0
- package/dist/src/mcp/client.js +215 -0
- package/dist/src/mcp/discovery.js +226 -0
- package/dist/src/mcp/policy.js +65 -0
- package/dist/src/mcp/registry.js +129 -0
- package/dist/src/mcp/service.js +460 -0
- package/dist/src/middleware/auth.js +179 -0
- package/dist/src/middleware/requestCapture.js +192 -0
- package/dist/src/middleware/requestStats.js +118 -0
- package/dist/src/pools/builder.js +132 -0
- package/dist/src/pools/repository.js +69 -0
- package/dist/src/pools/scheduler.js +360 -0
- package/dist/src/pools/types.js +2 -0
- package/dist/src/protocols/adapters/dashscope.js +267 -0
- package/dist/src/protocols/adapters/inferenceV2.js +346 -0
- package/dist/src/protocols/adapters/openai.js +27 -0
- package/dist/src/protocols/registry.js +99 -0
- package/dist/src/protocols/types.js +2 -0
- package/dist/src/providers/health.js +153 -0
- package/dist/src/providers/importer.js +289 -0
- package/dist/src/providers/modelRegistry.js +313 -0
- package/dist/src/providers/repository.js +361 -0
- package/dist/src/providers/types.js +2 -0
- package/dist/src/routes/admin.js +531 -0
- package/dist/src/routes/audio.js +295 -0
- package/dist/src/routes/chat.js +240 -0
- package/dist/src/routes/embeddings.js +157 -0
- package/dist/src/routes/images.js +288 -0
- package/dist/src/routes/mcp.js +256 -0
- package/dist/src/routes/mcpService.js +100 -0
- package/dist/src/routes/models.js +48 -0
- package/dist/src/routes/responses.js +711 -0
- package/dist/src/routes/sessions.js +450 -0
- package/dist/src/routes/stats.js +270 -0
- package/dist/src/routes/ui.js +97 -0
- package/dist/src/routes/videos.js +107 -0
- package/dist/src/routing/router.js +338 -0
- package/dist/src/services/imageGeneration.js +280 -0
- package/dist/src/services/imageUnderstanding.js +352 -0
- package/dist/src/services/videoGeneration.js +79 -0
- package/dist/src/storage/captureRepository.js +1591 -0
- package/dist/src/storage/files.js +157 -0
- package/dist/src/storage/imageCache.js +346 -0
- package/dist/src/storage/repositories.js +388 -0
- package/dist/src/storage/sessionRepository.js +370 -0
- package/dist/src/storage/statsRepository.js +204 -0
- package/dist/src/transport/httpClient.js +126 -0
- package/dist/src/types.js +2 -0
- package/dist/src/utils/messageMedia.js +285 -0
- package/dist/src/utils/modelCapabilities.js +108 -0
- package/dist/src/utils/modelDiscovery.js +170 -0
- package/dist/src/version.js +5 -0
- package/dist/src/workers/captureRetention.js +25 -0
- package/dist/src/workers/configWatcher.js +91 -0
- package/dist/src/workers/healthChecker.js +21 -0
- package/dist/src/workers/statsRotation.js +41 -0
- package/docs/LLM/output_schema.md +312 -0
- package/docs/benchmark.md +208 -0
- package/docs/mcp-guidelines.md +125 -0
- package/docs/mcp-service.md +178 -0
- package/docs/opencode.md +86 -0
- package/docs/providers.md +79 -0
- package/examples/benchmark.config.yaml +28 -0
- package/examples/providers/alibaba-dashscope.yaml +88 -0
- package/examples/providers/alibaba-llm.yaml +64 -0
- package/examples/providers/alibaba-registry.yaml +7 -0
- package/examples/providers/inference-v2-ray.yaml +29 -0
- package/examples/scenarios/assets/omni-call-sample.wav +0 -0
- package/examples/scenarios/custom.jsonl +5 -0
- package/examples/scenarios/custom.yaml +40 -0
- package/model-form-v2.png +0 -0
- package/package.json +66 -0
- package/provider-form-v2.png +0 -0
- package/provider-form.png +0 -0
- package/scripts/manual-test.sh +11 -0
- package/scripts/version-from-git.js +23 -0
- package/src/benchmark/artifacts.ts +149 -0
- package/src/benchmark/capabilityClassifier.ts +99 -0
- package/src/benchmark/capabilityStore.ts +174 -0
- package/src/benchmark/config.ts +337 -0
- package/src/benchmark/gates.ts +164 -0
- package/src/benchmark/jobs.ts +312 -0
- package/src/benchmark/runner.ts +2519 -0
- package/src/benchmark/schema.ts +443 -0
- package/src/benchmark/suites.ts +323 -0
- package/src/benchmark/tinyQaDataset.ts +428 -0
- package/src/benchmark/types.ts +442 -0
- package/src/config.ts +44 -0
- package/src/index.ts +195 -0
- package/src/mcp/client.ts +305 -0
- package/src/mcp/discovery.ts +266 -0
- package/src/mcp/policy.ts +105 -0
- package/src/mcp/registry.ts +164 -0
- package/src/mcp/service.ts +611 -0
- package/src/middleware/auth.ts +251 -0
- package/src/middleware/requestCapture.ts +245 -0
- package/src/middleware/requestStats.ts +163 -0
- package/src/pools/builder.ts +159 -0
- package/src/pools/repository.ts +71 -0
- package/src/pools/scheduler.ts +425 -0
- package/src/pools/types.ts +117 -0
- package/src/protocols/adapters/dashscope.ts +335 -0
- package/src/protocols/adapters/inferenceV2.ts +428 -0
- package/src/protocols/adapters/openai.ts +32 -0
- package/src/protocols/registry.ts +117 -0
- package/src/protocols/types.ts +81 -0
- package/src/providers/health.ts +207 -0
- package/src/providers/importer.ts +402 -0
- package/src/providers/modelRegistry.ts +415 -0
- package/src/providers/repository.ts +439 -0
- package/src/providers/types.ts +113 -0
- package/src/routes/admin.ts +666 -0
- package/src/routes/audio.ts +372 -0
- package/src/routes/chat.ts +301 -0
- package/src/routes/embeddings.ts +197 -0
- package/src/routes/images.ts +356 -0
- package/src/routes/mcp.ts +320 -0
- package/src/routes/mcpService.ts +114 -0
- package/src/routes/models.ts +50 -0
- package/src/routes/responses.ts +872 -0
- package/src/routes/sessions.ts +558 -0
- package/src/routes/stats.ts +312 -0
- package/src/routes/ui.ts +96 -0
- package/src/routes/videos.ts +132 -0
- package/src/routing/router.ts +501 -0
- package/src/services/imageGeneration.ts +396 -0
- package/src/services/imageUnderstanding.ts +449 -0
- package/src/services/videoGeneration.ts +127 -0
- package/src/storage/captureRepository.ts +1835 -0
- package/src/storage/files.ts +178 -0
- package/src/storage/imageCache.ts +405 -0
- package/src/storage/repositories.ts +494 -0
- package/src/storage/sessionRepository.ts +419 -0
- package/src/storage/statsRepository.ts +238 -0
- package/src/transport/httpClient.ts +145 -0
- package/src/types.ts +322 -0
- package/src/utils/messageMedia.ts +293 -0
- package/src/utils/modelCapabilities.ts +161 -0
- package/src/utils/modelDiscovery.ts +203 -0
- package/src/workers/captureRetention.ts +25 -0
- package/src/workers/configWatcher.ts +115 -0
- package/src/workers/healthChecker.ts +22 -0
- package/src/workers/statsRotation.ts +49 -0
- package/tests/benchmarkAdminRoutes.test.ts +82 -0
- package/tests/benchmarkBasics.test.ts +116 -0
- package/tests/captureAdminRoutes.test.ts +420 -0
- package/tests/captureRepository.test.ts +797 -0
- package/tests/cliLegacyRewrite.test.ts +45 -0
- package/tests/imageGeneration.service.test.ts +107 -0
- package/tests/imageUnderstanding.service.test.ts +123 -0
- package/tests/mcpPolicy.test.ts +105 -0
- package/tests/mcpService.test.ts +1245 -0
- package/tests/modelRef.test.ts +23 -0
- package/tests/modelsRoutes.test.ts +154 -0
- package/tests/sessionMediaCache.test.ts +167 -0
- package/tests/statsRoutes.test.ts +323 -0
- package/tsconfig.json +15 -0
- package/ui/index.html +16 -0
- package/ui/package-lock.json +8521 -0
- package/ui/package.json +52 -0
- package/ui/postcss.config.js +6 -0
- package/ui/public/assets/apple-touch-icon.png +0 -0
- package/ui/public/assets/favicon-16.png +0 -0
- package/ui/public/assets/favicon-32.png +0 -0
- package/ui/public/assets/icon-192.png +0 -0
- package/ui/public/assets/icon-512.png +0 -0
- package/ui/src/App.tsx +27 -0
- package/ui/src/api/client.ts +1503 -0
- package/ui/src/components/EndpointUsageGuide.tsx +361 -0
- package/ui/src/components/Layout.tsx +124 -0
- package/ui/src/components/MessageContent.tsx +365 -0
- package/ui/src/components/ToolCallMessage.tsx +179 -0
- package/ui/src/components/ToolPicker.tsx +442 -0
- package/ui/src/components/messageContentParser.test.ts +41 -0
- package/ui/src/components/messageContentParser.ts +73 -0
- package/ui/src/components/thinkingPreview.test.ts +27 -0
- package/ui/src/components/thinkingPreview.ts +15 -0
- package/ui/src/components/toMermaidSankey.test.ts +78 -0
- package/ui/src/components/toMermaidSankey.ts +56 -0
- package/ui/src/components/ui/button.tsx +58 -0
- package/ui/src/components/ui/input.tsx +21 -0
- package/ui/src/components/ui/textarea.tsx +21 -0
- package/ui/src/lib/utils.ts +6 -0
- package/ui/src/main.tsx +9 -0
- package/ui/src/pages/AgentPlayground.tsx +2010 -0
- package/ui/src/pages/Benchmark.tsx +988 -0
- package/ui/src/pages/Dashboard.tsx +581 -0
- package/ui/src/pages/Peek.tsx +962 -0
- package/ui/src/pages/Settings.tsx +2013 -0
- package/ui/src/pages/agentPlaygroundPayload.test.ts +109 -0
- package/ui/src/pages/agentPlaygroundPayload.ts +97 -0
- package/ui/src/pages/agentThinkingContent.test.ts +50 -0
- package/ui/src/pages/agentThinkingContent.ts +57 -0
- package/ui/src/pages/dashboardTokenUsage.test.ts +66 -0
- package/ui/src/pages/dashboardTokenUsage.ts +36 -0
- package/ui/src/pages/imageUpload.test.ts +39 -0
- package/ui/src/pages/imageUpload.ts +71 -0
- package/ui/src/pages/peekFilters.test.ts +29 -0
- package/ui/src/pages/peekFilters.ts +13 -0
- package/ui/src/pages/peekMedia.test.ts +58 -0
- package/ui/src/pages/peekMedia.ts +148 -0
- package/ui/src/pages/sessionAutoTitle.test.ts +128 -0
- package/ui/src/pages/sessionAutoTitle.ts +106 -0
- package/ui/src/stores/settings.ts +58 -0
- package/ui/src/styles/globals.css +223 -0
- package/ui/src/vite-env.d.ts +8 -0
- package/ui/tailwind.config.js +106 -0
- package/ui/tsconfig.json +32 -0
- package/ui/vite.config.ts +37 -0
|
@@ -0,0 +1,711 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.registerResponsesRoutes = registerResponsesRoutes;
|
|
4
|
+
const crypto_1 = require("crypto");
|
|
5
|
+
const stream_1 = require("stream");
|
|
6
|
+
const router_1 = require("../routing/router");
|
|
7
|
+
const repositories_1 = require("../storage/repositories");
|
|
8
|
+
const scheduler_1 = require("../pools/scheduler");
|
|
9
|
+
const modelRegistry_1 = require("../providers/modelRegistry");
|
|
10
|
+
const messageMedia_1 = require("../utils/messageMedia");
|
|
11
|
+
const requestCapture_1 = require("../middleware/requestCapture");
|
|
12
|
+
const requestStats_1 = require("../middleware/requestStats");
|
|
13
|
+
/**
|
|
14
|
+
* Responses API compatibility shim.
|
|
15
|
+
*
|
|
16
|
+
* Some newer SDK flows prefer the "Responses API" pattern. This endpoint
|
|
17
|
+
* translates those requests to /v1/chat/completions internally.
|
|
18
|
+
*
|
|
19
|
+
* Input formats supported:
|
|
20
|
+
* - { input: "string" } → single user message
|
|
21
|
+
* - { input: [{ role, content }] } → message array
|
|
22
|
+
* - { instructions: "..." } → system message prepended
|
|
23
|
+
*/
|
|
24
|
+
async function registerResponsesRoutes(app, paths) {
|
|
25
|
+
app.post("/v1/responses", async (req, reply) => {
|
|
26
|
+
const body = req.body;
|
|
27
|
+
if (!body?.model) {
|
|
28
|
+
const fallback = await pickDefaultModel(paths);
|
|
29
|
+
if (!fallback) {
|
|
30
|
+
reply.code(400).send({ error: { message: "model is required" } });
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (body)
|
|
34
|
+
body.model = fallback;
|
|
35
|
+
}
|
|
36
|
+
if (!body?.input) {
|
|
37
|
+
reply.code(400).send({ error: { message: "input is required" } });
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
// Transform to chat completions format
|
|
41
|
+
const messages = transformToMessages(body);
|
|
42
|
+
const transformedTools = body.tools ? transformTools(body.tools) : undefined;
|
|
43
|
+
// Track if client wants streaming
|
|
44
|
+
const clientWantsStreaming = body.stream ?? false;
|
|
45
|
+
const normalizedMessages = await (0, messageMedia_1.normalizeMessagesForUpstream)(paths, messages);
|
|
46
|
+
const media = (0, messageMedia_1.scanMessageModalities)(normalizedMessages);
|
|
47
|
+
const chatPayload = {
|
|
48
|
+
model: body.model,
|
|
49
|
+
messages: normalizedMessages,
|
|
50
|
+
stream: clientWantsStreaming, // Pass through streaming preference
|
|
51
|
+
temperature: body.temperature,
|
|
52
|
+
top_p: body.top_p,
|
|
53
|
+
max_tokens: body.max_tokens,
|
|
54
|
+
presence_penalty: body.presence_penalty,
|
|
55
|
+
frequency_penalty: body.frequency_penalty,
|
|
56
|
+
seed: body.seed,
|
|
57
|
+
stop: body.stop,
|
|
58
|
+
tools: transformedTools,
|
|
59
|
+
tool_choice: body.tool_choice
|
|
60
|
+
};
|
|
61
|
+
(0, requestCapture_1.setCaptureDerivedRequest)(reply, {
|
|
62
|
+
originalRequest: body,
|
|
63
|
+
normalizedRequest: chatPayload,
|
|
64
|
+
});
|
|
65
|
+
const requestId = (0, crypto_1.randomUUID)();
|
|
66
|
+
const start = Date.now();
|
|
67
|
+
const controller = new AbortController();
|
|
68
|
+
req.raw.on("close", () => controller.abort());
|
|
69
|
+
try {
|
|
70
|
+
const outcome = await (0, router_1.routeRequest)(paths, body.model, "/v1/chat/completions", chatPayload, req.headers, controller.signal, {
|
|
71
|
+
requiredInput: media.hasAudio
|
|
72
|
+
? media.hasImage
|
|
73
|
+
? ["text", "image", "audio"]
|
|
74
|
+
: ["text", "audio"]
|
|
75
|
+
: media.hasImage
|
|
76
|
+
? ["text", "image"]
|
|
77
|
+
: ["text"],
|
|
78
|
+
requiredOutput: ["text"],
|
|
79
|
+
});
|
|
80
|
+
// Handle streaming response
|
|
81
|
+
if (clientWantsStreaming) {
|
|
82
|
+
await streamResponsesAPI(reply, outcome.attempt.response, requestId, body.model);
|
|
83
|
+
(0, requestCapture_1.setCaptureResponseOverride)(reply, {
|
|
84
|
+
$type: "stream",
|
|
85
|
+
contentType: "text/event-stream",
|
|
86
|
+
note: "Responses API SSE stream captured as metadata",
|
|
87
|
+
}, outcome.attempt.response.headers);
|
|
88
|
+
(0, requestCapture_1.setCaptureRouting)(reply, {
|
|
89
|
+
publicModel: body.model,
|
|
90
|
+
endpointId: outcome.attempt.endpoint.id,
|
|
91
|
+
endpointName: outcome.attempt.endpoint.name,
|
|
92
|
+
upstreamModel: outcome.attempt.upstreamModel,
|
|
93
|
+
});
|
|
94
|
+
(0, requestStats_1.setStatsPayload)(reply, {
|
|
95
|
+
endpointId: outcome.attempt.endpoint.id,
|
|
96
|
+
endpointName: outcome.attempt.endpoint.name,
|
|
97
|
+
upstreamModel: outcome.attempt.upstreamModel,
|
|
98
|
+
});
|
|
99
|
+
await (0, repositories_1.logRequest)(paths, buildLog(requestId, body.model, outcome, Date.now() - start, true, 0 // Token count not available in streaming
|
|
100
|
+
));
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
// Non-streaming response
|
|
104
|
+
const upstreamBody = await readBody(outcome.attempt.response);
|
|
105
|
+
// Transform response to Responses API format
|
|
106
|
+
const responsesFormat = transformToResponsesFormat(upstreamBody.payload, requestId);
|
|
107
|
+
setHeaders(reply, outcome.attempt.response.headers);
|
|
108
|
+
reply.code(outcome.attempt.response.statusCode).send(responsesFormat);
|
|
109
|
+
(0, requestCapture_1.setCaptureRouting)(reply, {
|
|
110
|
+
publicModel: body.model,
|
|
111
|
+
endpointId: outcome.attempt.endpoint.id,
|
|
112
|
+
endpointName: outcome.attempt.endpoint.name,
|
|
113
|
+
upstreamModel: outcome.attempt.upstreamModel,
|
|
114
|
+
});
|
|
115
|
+
(0, requestStats_1.setStatsPayload)(reply, {
|
|
116
|
+
endpointId: outcome.attempt.endpoint.id,
|
|
117
|
+
endpointName: outcome.attempt.endpoint.name,
|
|
118
|
+
upstreamModel: outcome.attempt.upstreamModel,
|
|
119
|
+
totalTokens: upstreamBody.totalTokens,
|
|
120
|
+
promptTokens: upstreamBody.promptTokens,
|
|
121
|
+
completionTokens: upstreamBody.completionTokens,
|
|
122
|
+
});
|
|
123
|
+
await (0, repositories_1.logRequest)(paths, buildLog(requestId, body.model, outcome, Date.now() - start, false, upstreamBody.totalTokens));
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
const errorType = error.type ?? error.name;
|
|
127
|
+
(0, requestCapture_1.setCaptureError)(reply, { type: errorType, message: error.message });
|
|
128
|
+
await (0, repositories_1.logRequest)(paths, {
|
|
129
|
+
requestId,
|
|
130
|
+
ts: new Date(),
|
|
131
|
+
route: { publicModel: body?.model ?? "unknown" },
|
|
132
|
+
request: { stream: Boolean(body?.stream) },
|
|
133
|
+
result: { errorType, errorMessage: error.message }
|
|
134
|
+
});
|
|
135
|
+
// Don't try to send error if headers already sent (streaming started)
|
|
136
|
+
if (reply.raw.headersSent) {
|
|
137
|
+
req.log.warn({ err: error }, "Error after streaming started");
|
|
138
|
+
reply.raw.end();
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
if (errorType === "invalid_request") {
|
|
142
|
+
reply.code(400).send({ error: { message: error.message } });
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
if (errorType === "tls_verify_failed") {
|
|
146
|
+
reply.code(502).send({ error: { message: error.message } });
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
const status = errorType === "no_endpoints" ||
|
|
150
|
+
errorType === "protocol_stream_unsupported" ||
|
|
151
|
+
errorType === "unsupported_protocol" ||
|
|
152
|
+
errorType === "invalid_protocol_config"
|
|
153
|
+
? 400
|
|
154
|
+
: errorType === "rate_limited"
|
|
155
|
+
? 429
|
|
156
|
+
: 502;
|
|
157
|
+
reply.code(status).send({ error: { message: "Upstream unavailable" } });
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Transform Responses-style input to OpenAI chat completions messages.
|
|
163
|
+
*
|
|
164
|
+
* Some clients send a variety of item types:
|
|
165
|
+
* - { type: "message", role: "user/assistant/developer", content: [...] }
|
|
166
|
+
* - { type: "function_call", name: "...", arguments: "...", call_id: "..." }
|
|
167
|
+
* - { type: "function_call_output", call_id: "...", output: "..." }
|
|
168
|
+
*
|
|
169
|
+
* OpenAI chat completions expects:
|
|
170
|
+
* - { role: "user/assistant/system", content: "..." }
|
|
171
|
+
* - Assistant messages can have tool_calls: [{ id, type: "function", function: { name, arguments } }]
|
|
172
|
+
* - { role: "tool", tool_call_id: "...", content: "..." }
|
|
173
|
+
*/
|
|
174
|
+
function transformToMessages(body) {
|
|
175
|
+
const messages = [];
|
|
176
|
+
// Add system message from instructions if present
|
|
177
|
+
if (body.instructions) {
|
|
178
|
+
messages.push({ role: "system", content: body.instructions });
|
|
179
|
+
}
|
|
180
|
+
// Transform input
|
|
181
|
+
if (typeof body.input === "string") {
|
|
182
|
+
messages.push({ role: "user", content: body.input });
|
|
183
|
+
}
|
|
184
|
+
else if (Array.isArray(body.input)) {
|
|
185
|
+
// Process items, grouping consecutive function_calls into a single assistant message
|
|
186
|
+
let pendingToolCalls = [];
|
|
187
|
+
for (const item of body.input) {
|
|
188
|
+
if (!item || typeof item !== "object")
|
|
189
|
+
continue;
|
|
190
|
+
const itemObj = item;
|
|
191
|
+
const itemType = itemObj.type;
|
|
192
|
+
// Handle function_call items - need to be grouped into an assistant message
|
|
193
|
+
if (itemType === "function_call") {
|
|
194
|
+
pendingToolCalls.push({
|
|
195
|
+
id: itemObj.call_id || itemObj.id || "",
|
|
196
|
+
type: "function",
|
|
197
|
+
function: {
|
|
198
|
+
name: itemObj.name,
|
|
199
|
+
arguments: itemObj.arguments
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
// Before processing other items, flush any pending tool calls
|
|
205
|
+
if (pendingToolCalls.length > 0) {
|
|
206
|
+
messages.push({
|
|
207
|
+
role: "assistant",
|
|
208
|
+
content: null,
|
|
209
|
+
tool_calls: pendingToolCalls
|
|
210
|
+
});
|
|
211
|
+
pendingToolCalls = [];
|
|
212
|
+
}
|
|
213
|
+
// Handle function_call_output items - become tool role messages
|
|
214
|
+
if (itemType === "function_call_output") {
|
|
215
|
+
messages.push({
|
|
216
|
+
role: "tool",
|
|
217
|
+
tool_call_id: itemObj.call_id,
|
|
218
|
+
content: typeof itemObj.output === "string" ? itemObj.output : JSON.stringify(itemObj.output)
|
|
219
|
+
});
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
// Handle regular message items
|
|
223
|
+
if (itemType === "message" && "role" in itemObj && "content" in itemObj) {
|
|
224
|
+
const role = itemObj.role;
|
|
225
|
+
// Map developer role to system
|
|
226
|
+
const mappedRole = role === "developer" ? "system" : role;
|
|
227
|
+
const content = transformMessageContent(itemObj.content);
|
|
228
|
+
messages.push({ role: mappedRole, content });
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
// Handle items with role/content directly (legacy format)
|
|
232
|
+
if ("role" in itemObj && "content" in itemObj) {
|
|
233
|
+
const role = itemObj.role;
|
|
234
|
+
const mappedRole = role === "developer" ? "system" : role;
|
|
235
|
+
const content = transformMessageContent(itemObj.content);
|
|
236
|
+
messages.push({ role: mappedRole, content });
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
// Flush any remaining pending tool calls
|
|
241
|
+
if (pendingToolCalls.length > 0) {
|
|
242
|
+
messages.push({
|
|
243
|
+
role: "assistant",
|
|
244
|
+
content: null,
|
|
245
|
+
tool_calls: pendingToolCalls
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return messages;
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Transform message content, normalizing response content part types to OpenAI format.
|
|
253
|
+
* Some clients send: { type: "input_text", text: "..." } for user messages
|
|
254
|
+
* Some clients send: { type: "output_text", text: "..." } for assistant messages
|
|
255
|
+
* OpenAI expects: { type: "text", text: "..." }
|
|
256
|
+
*/
|
|
257
|
+
function transformMessageContent(content) {
|
|
258
|
+
if (typeof content === "string") {
|
|
259
|
+
return content;
|
|
260
|
+
}
|
|
261
|
+
if (Array.isArray(content)) {
|
|
262
|
+
return content.map(part => {
|
|
263
|
+
if (part && typeof part === "object") {
|
|
264
|
+
const p = part;
|
|
265
|
+
// Normalize input_text/output_text to OpenAI text
|
|
266
|
+
// input_text is typically user content, output_text assistant content
|
|
267
|
+
if (p.type === "input_text" || p.type === "output_text") {
|
|
268
|
+
return { ...p, type: "text" };
|
|
269
|
+
}
|
|
270
|
+
if (p.type === "input_image" && p.image_url) {
|
|
271
|
+
return { ...p, type: "image_url" };
|
|
272
|
+
}
|
|
273
|
+
// Accept shorthand {type:\"audio\", audio:\"...\"} and normalize downstream
|
|
274
|
+
if (p.type === "input_audio" || p.type === "audio" || p.type === "video") {
|
|
275
|
+
return p;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
return part;
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
// Fallback: return as array containing the original content
|
|
282
|
+
return [content];
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Transform Responses-style tools to OpenAI function-calling format.
|
|
286
|
+
*
|
|
287
|
+
* Some clients send tools like:
|
|
288
|
+
* { type: "function", name: "...", description: "...", parameters: {...} }
|
|
289
|
+
*
|
|
290
|
+
* OpenAI expects:
|
|
291
|
+
* { type: "function", function: { name: "...", description: "...", parameters: {...} } }
|
|
292
|
+
*
|
|
293
|
+
* Special case: web_search tools are filtered out as they're not supported by OpenAI format.
|
|
294
|
+
*/
|
|
295
|
+
function transformTools(tools) {
|
|
296
|
+
return tools
|
|
297
|
+
.filter(tool => {
|
|
298
|
+
// Filter out web_search tools - not supported in OpenAI function calling format
|
|
299
|
+
if (tool && typeof tool === "object") {
|
|
300
|
+
const t = tool;
|
|
301
|
+
if (t.type === "web_search") {
|
|
302
|
+
return false;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
return true;
|
|
306
|
+
})
|
|
307
|
+
.map(tool => {
|
|
308
|
+
if (!tool || typeof tool !== "object")
|
|
309
|
+
return tool;
|
|
310
|
+
const t = tool;
|
|
311
|
+
// If already in OpenAI format (has 'function' property), return as-is
|
|
312
|
+
if (t.function)
|
|
313
|
+
return tool;
|
|
314
|
+
// If has type="function" but no 'function' wrapper, wrap it
|
|
315
|
+
if (t.type === "function") {
|
|
316
|
+
const { type, ...functionDef } = t;
|
|
317
|
+
return {
|
|
318
|
+
type,
|
|
319
|
+
function: functionDef
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
// Otherwise return unchanged
|
|
323
|
+
return tool;
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
function transformToResponsesFormat(chatResponse, requestId) {
|
|
327
|
+
if (!chatResponse || typeof chatResponse !== "object") {
|
|
328
|
+
return {
|
|
329
|
+
id: requestId,
|
|
330
|
+
object: "response",
|
|
331
|
+
created_at: Math.floor(Date.now() / 1000),
|
|
332
|
+
output: []
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
const chat = chatResponse;
|
|
336
|
+
const firstChoice = chat.choices?.[0];
|
|
337
|
+
const message = firstChoice?.message;
|
|
338
|
+
const output = [];
|
|
339
|
+
// Handle tool calls if present
|
|
340
|
+
if (message?.tool_calls && message.tool_calls.length > 0) {
|
|
341
|
+
for (const toolCall of message.tool_calls) {
|
|
342
|
+
output.push({
|
|
343
|
+
type: "function_call",
|
|
344
|
+
id: toolCall.id,
|
|
345
|
+
call_id: toolCall.id,
|
|
346
|
+
name: toolCall.function.name,
|
|
347
|
+
arguments: toolCall.function.arguments
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
// Handle text content
|
|
352
|
+
const textContent = message?.content ?? "";
|
|
353
|
+
if (textContent || output.length === 0) {
|
|
354
|
+
output.push({
|
|
355
|
+
type: "message",
|
|
356
|
+
role: message?.role ?? "assistant",
|
|
357
|
+
// Responses-style clients may expect output_text instead of text
|
|
358
|
+
content: [{ type: "output_text", text: textContent }]
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
return {
|
|
362
|
+
id: chat.id ?? requestId,
|
|
363
|
+
object: "response",
|
|
364
|
+
created_at: chat.created ?? Math.floor(Date.now() / 1000),
|
|
365
|
+
model: chat.model,
|
|
366
|
+
output,
|
|
367
|
+
usage: chat.usage ? {
|
|
368
|
+
input_tokens: chat.usage.prompt_tokens ?? 0,
|
|
369
|
+
output_tokens: chat.usage.completion_tokens ?? 0,
|
|
370
|
+
total_tokens: chat.usage.total_tokens ?? 0
|
|
371
|
+
} : undefined
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* Stream chat completions response and transform to Responses API SSE format.
|
|
376
|
+
*
|
|
377
|
+
* This reads the upstream SSE stream (chat.completion.chunk format) and
|
|
378
|
+
* transforms it to Responses API format in real-time.
|
|
379
|
+
*/
|
|
380
|
+
async function streamResponsesAPI(reply, upstreamResponse, requestId, model) {
|
|
381
|
+
reply.raw.writeHead(200, {
|
|
382
|
+
"Content-Type": "text/event-stream",
|
|
383
|
+
"Cache-Control": "no-cache",
|
|
384
|
+
"Connection": "keep-alive"
|
|
385
|
+
});
|
|
386
|
+
const sendEvent = (eventType, data) => {
|
|
387
|
+
reply.raw.write(`event: ${eventType}\n`);
|
|
388
|
+
reply.raw.write(`data: ${JSON.stringify(data)}\n\n`);
|
|
389
|
+
};
|
|
390
|
+
// Send response.created immediately
|
|
391
|
+
sendEvent("response.created", {
|
|
392
|
+
type: "response.created",
|
|
393
|
+
response: {
|
|
394
|
+
id: requestId,
|
|
395
|
+
object: "response",
|
|
396
|
+
created_at: Math.floor(Date.now() / 1000),
|
|
397
|
+
model,
|
|
398
|
+
output: [],
|
|
399
|
+
usage: null
|
|
400
|
+
}
|
|
401
|
+
});
|
|
402
|
+
// Accumulate content and tool calls for the final response
|
|
403
|
+
let accumulatedContent = "";
|
|
404
|
+
let accumulatedToolCalls = [];
|
|
405
|
+
let usage = null;
|
|
406
|
+
let currentToolCallIndex = -1;
|
|
407
|
+
try {
|
|
408
|
+
const body = upstreamResponse.body;
|
|
409
|
+
if (!body) {
|
|
410
|
+
throw new Error("No response body");
|
|
411
|
+
}
|
|
412
|
+
// Convert to async iterable
|
|
413
|
+
const reader = 'getReader' in body
|
|
414
|
+
? body.getReader()
|
|
415
|
+
: null;
|
|
416
|
+
let buffer = "";
|
|
417
|
+
const processChunk = (text) => {
|
|
418
|
+
buffer += text;
|
|
419
|
+
const lines = buffer.split("\n");
|
|
420
|
+
buffer = lines.pop() || ""; // Keep incomplete line in buffer
|
|
421
|
+
for (const line of lines) {
|
|
422
|
+
if (line.startsWith("data: ")) {
|
|
423
|
+
const data = line.slice(6).trim();
|
|
424
|
+
if (data === "[DONE]") {
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
try {
|
|
428
|
+
const chunk = JSON.parse(data);
|
|
429
|
+
const delta = chunk.choices?.[0]?.delta;
|
|
430
|
+
if (delta) {
|
|
431
|
+
// Handle reasoning/thinking content delta
|
|
432
|
+
if (delta.reasoning_content || delta.reasoning) {
|
|
433
|
+
const reasoningDelta = delta.reasoning_content || delta.reasoning;
|
|
434
|
+
// Send reasoning delta event
|
|
435
|
+
sendEvent("response.reasoning_text.delta", {
|
|
436
|
+
type: "response.reasoning_text.delta",
|
|
437
|
+
output_index: 0,
|
|
438
|
+
content_index: 0,
|
|
439
|
+
delta: reasoningDelta
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
// Handle content delta
|
|
443
|
+
if (delta.content) {
|
|
444
|
+
accumulatedContent += delta.content;
|
|
445
|
+
// Send content delta event
|
|
446
|
+
sendEvent("response.output_text.delta", {
|
|
447
|
+
type: "response.output_text.delta",
|
|
448
|
+
output_index: 0,
|
|
449
|
+
content_index: 0,
|
|
450
|
+
delta: delta.content
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
// Handle tool calls delta
|
|
454
|
+
if (delta.tool_calls) {
|
|
455
|
+
for (const toolCallDelta of delta.tool_calls) {
|
|
456
|
+
const idx = toolCallDelta.index;
|
|
457
|
+
if (idx !== currentToolCallIndex) {
|
|
458
|
+
currentToolCallIndex = idx;
|
|
459
|
+
accumulatedToolCalls[idx] = {
|
|
460
|
+
id: toolCallDelta.id || "",
|
|
461
|
+
name: toolCallDelta.function?.name || "",
|
|
462
|
+
arguments: ""
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
if (toolCallDelta.id) {
|
|
466
|
+
accumulatedToolCalls[idx].id = toolCallDelta.id;
|
|
467
|
+
}
|
|
468
|
+
if (toolCallDelta.function?.name) {
|
|
469
|
+
accumulatedToolCalls[idx].name = toolCallDelta.function.name;
|
|
470
|
+
}
|
|
471
|
+
if (toolCallDelta.function?.arguments) {
|
|
472
|
+
accumulatedToolCalls[idx].arguments += toolCallDelta.function.arguments;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
// Capture usage from final chunk
|
|
478
|
+
if (chunk.usage) {
|
|
479
|
+
usage = {
|
|
480
|
+
input_tokens: chunk.usage.prompt_tokens ?? 0,
|
|
481
|
+
output_tokens: chunk.usage.completion_tokens ?? 0,
|
|
482
|
+
total_tokens: chunk.usage.total_tokens ?? 0
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
catch (e) {
|
|
487
|
+
// Ignore parse errors for malformed chunks
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
};
|
|
492
|
+
if (reader) {
|
|
493
|
+
// Web Streams API (ReadableStream)
|
|
494
|
+
const decoder = new TextDecoder();
|
|
495
|
+
while (true) {
|
|
496
|
+
const { done, value } = await reader.read();
|
|
497
|
+
if (done)
|
|
498
|
+
break;
|
|
499
|
+
processChunk(decoder.decode(value, { stream: true }));
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
else {
|
|
503
|
+
// Node.js stream
|
|
504
|
+
const nodeStream = body;
|
|
505
|
+
for await (const chunk of nodeStream) {
|
|
506
|
+
processChunk(chunk.toString());
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
// Build final output
|
|
510
|
+
const output = [];
|
|
511
|
+
// Add tool calls first
|
|
512
|
+
for (const tc of accumulatedToolCalls) {
|
|
513
|
+
if (tc) {
|
|
514
|
+
output.push({
|
|
515
|
+
type: "function_call",
|
|
516
|
+
id: tc.id,
|
|
517
|
+
call_id: tc.id,
|
|
518
|
+
name: tc.name,
|
|
519
|
+
arguments: tc.arguments
|
|
520
|
+
});
|
|
521
|
+
// Send output_item.done for each tool call
|
|
522
|
+
sendEvent("response.output_item.done", {
|
|
523
|
+
type: "response.output_item.done",
|
|
524
|
+
output_index: output.length - 1,
|
|
525
|
+
item: output[output.length - 1]
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
// Add message content if any
|
|
530
|
+
if (accumulatedContent || output.length === 0) {
|
|
531
|
+
output.push({
|
|
532
|
+
type: "message",
|
|
533
|
+
role: "assistant",
|
|
534
|
+
content: [{ type: "output_text", text: accumulatedContent }]
|
|
535
|
+
});
|
|
536
|
+
// Send output_item.done for the message
|
|
537
|
+
sendEvent("response.output_item.done", {
|
|
538
|
+
type: "response.output_item.done",
|
|
539
|
+
output_index: output.length - 1,
|
|
540
|
+
item: output[output.length - 1]
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
// Send response.completed
|
|
544
|
+
sendEvent("response.completed", {
|
|
545
|
+
type: "response.completed",
|
|
546
|
+
response: {
|
|
547
|
+
id: requestId,
|
|
548
|
+
object: "response",
|
|
549
|
+
created_at: Math.floor(Date.now() / 1000),
|
|
550
|
+
model,
|
|
551
|
+
output,
|
|
552
|
+
usage
|
|
553
|
+
}
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
catch (error) {
|
|
557
|
+
console.error("[responses] Streaming error:", error);
|
|
558
|
+
// Send error as part of the stream
|
|
559
|
+
sendEvent("error", {
|
|
560
|
+
type: "error",
|
|
561
|
+
error: { message: error.message }
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
reply.raw.end();
|
|
565
|
+
}
|
|
566
|
+
/**
|
|
567
|
+
* Send response as Server-Sent Events in Responses format.
|
|
568
|
+
*
|
|
569
|
+
* Responses-style clients expect:
|
|
570
|
+
* - event: response.created
|
|
571
|
+
* - event: response.output_item.done (for each output item)
|
|
572
|
+
* - event: response.completed
|
|
573
|
+
*
|
|
574
|
+
* Each event has:
|
|
575
|
+
* - event: <event_type>
|
|
576
|
+
* - data: {"type":"<event_type>", ...payload}
|
|
577
|
+
*/
|
|
578
|
+
async function sendAsSSE(reply, response) {
|
|
579
|
+
reply.raw.writeHead(200, {
|
|
580
|
+
"Content-Type": "text/event-stream",
|
|
581
|
+
"Cache-Control": "no-cache",
|
|
582
|
+
"Connection": "keep-alive"
|
|
583
|
+
});
|
|
584
|
+
// Helper to send an SSE event
|
|
585
|
+
const sendEvent = (eventType, data) => {
|
|
586
|
+
reply.raw.write(`event: ${eventType}\n`);
|
|
587
|
+
reply.raw.write(`data: ${JSON.stringify(data)}\n\n`);
|
|
588
|
+
};
|
|
589
|
+
// 1. response.created
|
|
590
|
+
sendEvent("response.created", {
|
|
591
|
+
type: "response.created",
|
|
592
|
+
response: {
|
|
593
|
+
id: response.id,
|
|
594
|
+
object: response.object,
|
|
595
|
+
created_at: response.created_at,
|
|
596
|
+
model: response.model,
|
|
597
|
+
output: [],
|
|
598
|
+
usage: null
|
|
599
|
+
}
|
|
600
|
+
});
|
|
601
|
+
// 2. response.output_item.done for each output item
|
|
602
|
+
for (let i = 0; i < response.output.length; i++) {
|
|
603
|
+
const item = response.output[i];
|
|
604
|
+
sendEvent("response.output_item.done", {
|
|
605
|
+
type: "response.output_item.done",
|
|
606
|
+
output_index: i,
|
|
607
|
+
item
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
// 3. response.completed
|
|
611
|
+
sendEvent("response.completed", {
|
|
612
|
+
type: "response.completed",
|
|
613
|
+
response: {
|
|
614
|
+
id: response.id,
|
|
615
|
+
object: response.object,
|
|
616
|
+
created_at: response.created_at,
|
|
617
|
+
model: response.model,
|
|
618
|
+
output: response.output,
|
|
619
|
+
usage: response.usage
|
|
620
|
+
}
|
|
621
|
+
});
|
|
622
|
+
reply.raw.end();
|
|
623
|
+
}
|
|
624
|
+
async function pickDefaultModel(paths) {
|
|
625
|
+
const smart = await (0, scheduler_1.selectPoolCandidates)(paths, "smart", {
|
|
626
|
+
requiredInput: ["text"],
|
|
627
|
+
requiredOutput: ["text"],
|
|
628
|
+
}, {
|
|
629
|
+
operation: "chat_completions",
|
|
630
|
+
stream: false,
|
|
631
|
+
});
|
|
632
|
+
if (smart && smart.candidates.length > 0) {
|
|
633
|
+
return "smart";
|
|
634
|
+
}
|
|
635
|
+
const byCapabilities = await (0, modelRegistry_1.pickBestProviderModelByCapabilities)(paths, { requiredInput: ["text"], requiredOutput: ["text"] }, "llm");
|
|
636
|
+
if (byCapabilities) {
|
|
637
|
+
return byCapabilities;
|
|
638
|
+
}
|
|
639
|
+
return null;
|
|
640
|
+
}
|
|
641
|
+
async function streamResponse(reply, response) {
|
|
642
|
+
const headers = normalizeHeaders(response.headers);
|
|
643
|
+
if (!headers["content-type"]) {
|
|
644
|
+
headers["content-type"] = "text/event-stream";
|
|
645
|
+
}
|
|
646
|
+
headers["cache-control"] = headers["cache-control"] ?? "no-cache";
|
|
647
|
+
reply.raw.writeHead(response.statusCode, headers);
|
|
648
|
+
await new Promise((resolve, reject) => {
|
|
649
|
+
(0, stream_1.pipeline)(response.body, reply.raw, (err) => {
|
|
650
|
+
if (err)
|
|
651
|
+
reject(err);
|
|
652
|
+
else
|
|
653
|
+
resolve();
|
|
654
|
+
});
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
function setHeaders(reply, headers) {
|
|
658
|
+
const normalized = normalizeHeaders(headers);
|
|
659
|
+
for (const [key, value] of Object.entries(normalized)) {
|
|
660
|
+
reply.header(key, value);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
function normalizeHeaders(headers) {
|
|
664
|
+
const normalized = {};
|
|
665
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
666
|
+
normalized[key.toLowerCase()] = Array.isArray(value) ? value.join(", ") : value;
|
|
667
|
+
}
|
|
668
|
+
return normalized;
|
|
669
|
+
}
|
|
670
|
+
async function readBody(response) {
|
|
671
|
+
const chunks = [];
|
|
672
|
+
for await (const chunk of response.body) {
|
|
673
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
674
|
+
}
|
|
675
|
+
const buffer = Buffer.concat(chunks);
|
|
676
|
+
const contentType = normalizeHeaders(response.headers)["content-type"] ?? "";
|
|
677
|
+
if (contentType.includes("application/json")) {
|
|
678
|
+
try {
|
|
679
|
+
const payload = JSON.parse(buffer.toString("utf8"));
|
|
680
|
+
const usage = typeof payload === "object" && payload && payload.usage;
|
|
681
|
+
return {
|
|
682
|
+
payload,
|
|
683
|
+
totalTokens: usage?.total_tokens ?? null,
|
|
684
|
+
promptTokens: usage?.prompt_tokens ?? null,
|
|
685
|
+
completionTokens: usage?.completion_tokens ?? null,
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
catch {
|
|
689
|
+
return { payload: buffer, totalTokens: null, promptTokens: null, completionTokens: null };
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
return { payload: buffer, totalTokens: null, promptTokens: null, completionTokens: null };
|
|
693
|
+
}
|
|
694
|
+
function buildLog(requestId, model, outcome, latencyMs, stream, totalTokens) {
|
|
695
|
+
return {
|
|
696
|
+
requestId,
|
|
697
|
+
ts: new Date(),
|
|
698
|
+
route: {
|
|
699
|
+
publicModel: model,
|
|
700
|
+
endpointId: outcome.attempt.endpoint.id,
|
|
701
|
+
endpointName: outcome.attempt.endpoint.name,
|
|
702
|
+
upstreamModel: outcome.attempt.upstreamModel
|
|
703
|
+
},
|
|
704
|
+
request: { stream },
|
|
705
|
+
result: {
|
|
706
|
+
statusCode: outcome.attempt.response.statusCode,
|
|
707
|
+
latencyMs,
|
|
708
|
+
totalTokens: totalTokens ?? null
|
|
709
|
+
}
|
|
710
|
+
};
|
|
711
|
+
}
|