openclaw-server 0.1.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/package.json +29 -0
- package/packs/default/faq.yaml +8 -0
- package/packs/default/intents.yaml +19 -0
- package/packs/default/pack.yaml +12 -0
- package/packs/default/policies.yaml +1 -0
- package/packs/default/scenarios.yaml +1 -0
- package/packs/default/synonyms.yaml +1 -0
- package/packs/default/templates.yaml +16 -0
- package/packs/default/tools.yaml +1 -0
- package/readme.md +1219 -0
- package/src/auth.ts +24 -0
- package/src/better-sqlite3.d.ts +17 -0
- package/src/config.ts +63 -0
- package/src/core/matcher.ts +214 -0
- package/src/core/normalizer.test.ts +37 -0
- package/src/core/normalizer.ts +183 -0
- package/src/core/pack-loader.ts +97 -0
- package/src/core/reply-engine.test.ts +76 -0
- package/src/core/reply-engine.ts +256 -0
- package/src/core/request-adapter.ts +65 -0
- package/src/core/session-store.ts +48 -0
- package/src/core/stream-renderer.ts +237 -0
- package/src/core/tool-engine.ts +60 -0
- package/src/debug-log.ts +211 -0
- package/src/index.ts +23 -0
- package/src/openai.ts +79 -0
- package/src/response-api.ts +107 -0
- package/src/routes/admin.ts +32 -0
- package/src/routes/chat-completions.ts +173 -0
- package/src/routes/health.ts +7 -0
- package/src/routes/models.ts +21 -0
- package/src/routes/request-validation.ts +33 -0
- package/src/routes/responses.ts +182 -0
- package/src/routes/tasks.ts +138 -0
- package/src/runtime-stats.ts +80 -0
- package/src/server.test.ts +776 -0
- package/src/server.ts +108 -0
- package/src/tasks/chat-integration.ts +70 -0
- package/src/tasks/service.ts +320 -0
- package/src/tasks/store.test.ts +183 -0
- package/src/tasks/store.ts +602 -0
- package/src/tasks/time-parser.test.ts +94 -0
- package/src/tasks/time-parser.ts +610 -0
- package/src/tasks/timezone.ts +171 -0
- package/src/tasks/types.ts +128 -0
- package/src/types.ts +202 -0
- package/src/weather/chat-integration.ts +56 -0
- package/src/weather/location-catalog.ts +166 -0
- package/src/weather/open-meteo-provider.ts +221 -0
- package/src/weather/parser.test.ts +23 -0
- package/src/weather/parser.ts +102 -0
- package/src/weather/service.test.ts +54 -0
- package/src/weather/service.ts +188 -0
- package/src/weather/types.ts +56 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import type {
|
|
3
|
+
ChatToolCall,
|
|
4
|
+
IntentDefinition,
|
|
5
|
+
LoadedPack,
|
|
6
|
+
NormalizedTurn,
|
|
7
|
+
ToolDefinition,
|
|
8
|
+
ToolStrategyDefinition,
|
|
9
|
+
} from "../types.js";
|
|
10
|
+
|
|
11
|
+
function requestedToolName(turn: NormalizedTurn): string | undefined {
|
|
12
|
+
if (!turn.toolChoice || typeof turn.toolChoice !== "object") {
|
|
13
|
+
return undefined;
|
|
14
|
+
}
|
|
15
|
+
return turn.toolChoice.function.name;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function pickProvidedTool(
|
|
19
|
+
tools: ToolDefinition[],
|
|
20
|
+
strategy: ToolStrategyDefinition,
|
|
21
|
+
): ToolDefinition | undefined {
|
|
22
|
+
return tools.find((tool) => strategy.preferredNames.includes(tool.function.name));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function selectToolCall(params: {
|
|
26
|
+
turn: NormalizedTurn;
|
|
27
|
+
intent: IntentDefinition;
|
|
28
|
+
pack: LoadedPack;
|
|
29
|
+
}): ChatToolCall | undefined {
|
|
30
|
+
if (!params.turn.tools.length || params.turn.toolChoice === "none") {
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
if (!params.intent.toolStrategyId) {
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const strategy = params.pack.toolStrategiesById.get(params.intent.toolStrategyId);
|
|
38
|
+
if (!strategy) {
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const picked = pickProvidedTool(params.turn.tools, strategy);
|
|
43
|
+
if (!picked) {
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const requiredToolName = requestedToolName(params.turn);
|
|
48
|
+
if (requiredToolName && requiredToolName !== picked.function.name) {
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
id: `call_${randomUUID().replace(/-/g, "").slice(0, 24)}`,
|
|
54
|
+
type: "function",
|
|
55
|
+
function: {
|
|
56
|
+
name: picked.function.name,
|
|
57
|
+
arguments: JSON.stringify(strategy.arguments ?? {}),
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
package/src/debug-log.ts
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { extractTextContent } from "./core/normalizer.js";
|
|
2
|
+
import type { TaskMessageInspection } from "./tasks/service.js";
|
|
3
|
+
import type { NormalizedTurn } from "./types.js";
|
|
4
|
+
import type { WeatherMessageInspection } from "./weather/types.js";
|
|
5
|
+
|
|
6
|
+
function previewText(text: string, limit: number): string {
|
|
7
|
+
const normalized = text.replace(/\s+/g, " ").trim();
|
|
8
|
+
if (normalized.length <= limit) {
|
|
9
|
+
return normalized;
|
|
10
|
+
}
|
|
11
|
+
return `${normalized.slice(0, limit)}...`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function summarizeContent(content: unknown, limit: number): string {
|
|
15
|
+
if (typeof content === "string") {
|
|
16
|
+
return `string:${previewText(content, limit)}`;
|
|
17
|
+
}
|
|
18
|
+
if (Array.isArray(content)) {
|
|
19
|
+
const types = content
|
|
20
|
+
.slice(0, 4)
|
|
21
|
+
.map((part) => {
|
|
22
|
+
if (!part || typeof part !== "object") {
|
|
23
|
+
return typeof part;
|
|
24
|
+
}
|
|
25
|
+
return String((part as { type?: unknown }).type ?? "object");
|
|
26
|
+
})
|
|
27
|
+
.join(",");
|
|
28
|
+
const text = extractTextContent(content);
|
|
29
|
+
return `array[${types || "empty"}]:${previewText(text, limit)}`;
|
|
30
|
+
}
|
|
31
|
+
if (content && typeof content === "object") {
|
|
32
|
+
const text = extractTextContent(content);
|
|
33
|
+
const keys = Object.keys(content as Record<string, unknown>).slice(0, 4).join(",");
|
|
34
|
+
return `object{${keys || "no-keys"}}:${previewText(text, limit)}`;
|
|
35
|
+
}
|
|
36
|
+
return String(content);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function summarizeMessages(messages: unknown[], limit: number): string {
|
|
40
|
+
if (messages.length === 0) {
|
|
41
|
+
return "[]";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const summary = messages
|
|
45
|
+
.slice(0, 5)
|
|
46
|
+
.map((message, index) => {
|
|
47
|
+
if (!message || typeof message !== "object") {
|
|
48
|
+
return `${index}:invalid`;
|
|
49
|
+
}
|
|
50
|
+
const record = message as { role?: unknown; content?: unknown };
|
|
51
|
+
const role = typeof record.role === "string" ? record.role : "unknown";
|
|
52
|
+
return `${index}:${role}:${summarizeContent(record.content, limit)}`;
|
|
53
|
+
})
|
|
54
|
+
.join(" | ");
|
|
55
|
+
|
|
56
|
+
if (messages.length <= 5) {
|
|
57
|
+
return summary;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return `${summary} | +${messages.length - 5} more`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function summarizeResponsesInput(input: unknown, limit: number): string {
|
|
64
|
+
const items = Array.isArray(input) ? input : [input];
|
|
65
|
+
return items
|
|
66
|
+
.slice(0, 5)
|
|
67
|
+
.map((item, index) => {
|
|
68
|
+
if (typeof item === "string") {
|
|
69
|
+
return `${index}:string:${previewText(item, limit)}`;
|
|
70
|
+
}
|
|
71
|
+
if (!item || typeof item !== "object") {
|
|
72
|
+
return `${index}:invalid`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const record = item as { type?: unknown; role?: unknown; content?: unknown; text?: unknown };
|
|
76
|
+
const type = typeof record.type === "string" ? record.type : "item";
|
|
77
|
+
const role = typeof record.role === "string" ? record.role : "user";
|
|
78
|
+
const content = record.content ?? record.text ?? item;
|
|
79
|
+
return `${index}:${type}:${role}:${summarizeContent(content, limit)}`;
|
|
80
|
+
})
|
|
81
|
+
.join(" | ");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function formatField(key: string, value: unknown): string | undefined {
|
|
85
|
+
if (value === undefined || value === null || value === "") {
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
if (typeof value === "number" || typeof value === "boolean") {
|
|
89
|
+
return `${key}=${value}`;
|
|
90
|
+
}
|
|
91
|
+
return `${key}=${JSON.stringify(String(value))}`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function logLine(namespace: string, fields: Record<string, unknown>): void {
|
|
95
|
+
const parts = Object.entries(fields)
|
|
96
|
+
.map(([key, value]) => formatField(key, value))
|
|
97
|
+
.filter((value): value is string => Boolean(value));
|
|
98
|
+
|
|
99
|
+
console.log(`[openclaw-server][${namespace}] ${parts.join(" ")}`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function logRequestDebug(params: {
|
|
103
|
+
enabled: boolean;
|
|
104
|
+
route: string;
|
|
105
|
+
body: unknown;
|
|
106
|
+
turn?: NormalizedTurn;
|
|
107
|
+
previewChars: number;
|
|
108
|
+
note?: string;
|
|
109
|
+
}): void {
|
|
110
|
+
if (!params.enabled || !params.body || typeof params.body !== "object") {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const record = params.body as {
|
|
115
|
+
model?: unknown;
|
|
116
|
+
user?: unknown;
|
|
117
|
+
stream?: unknown;
|
|
118
|
+
messages?: unknown;
|
|
119
|
+
input?: unknown;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
logLine("request", {
|
|
123
|
+
route: params.route,
|
|
124
|
+
note: params.note,
|
|
125
|
+
model: typeof record.model === "string" ? record.model : undefined,
|
|
126
|
+
user: typeof record.user === "string" ? record.user : undefined,
|
|
127
|
+
stream: typeof record.stream === "boolean" ? record.stream : undefined,
|
|
128
|
+
userText: params.turn ? previewText(params.turn.userText, params.previewChars) : undefined,
|
|
129
|
+
messages: Array.isArray(record.messages)
|
|
130
|
+
? summarizeMessages(record.messages, params.previewChars)
|
|
131
|
+
: undefined,
|
|
132
|
+
input:
|
|
133
|
+
record.input !== undefined
|
|
134
|
+
? summarizeResponsesInput(record.input, params.previewChars)
|
|
135
|
+
: undefined,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function logTaskInspection(params: {
|
|
140
|
+
enabled: boolean;
|
|
141
|
+
route: string;
|
|
142
|
+
userId: string;
|
|
143
|
+
text: string;
|
|
144
|
+
inspection: TaskMessageInspection;
|
|
145
|
+
previewChars: number;
|
|
146
|
+
}): void {
|
|
147
|
+
if (!params.enabled) {
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
logLine("task", {
|
|
152
|
+
route: params.route,
|
|
153
|
+
userId: params.userId,
|
|
154
|
+
handled: params.inspection.shouldHandle,
|
|
155
|
+
reason: params.inspection.reason,
|
|
156
|
+
queryScope: params.inspection.queryScope,
|
|
157
|
+
actionKind: params.inspection.actionKind,
|
|
158
|
+
draftKind: params.inspection.draftKind,
|
|
159
|
+
title: params.inspection.title,
|
|
160
|
+
dueAt: params.inspection.dueAtText,
|
|
161
|
+
text: previewText(params.text, params.previewChars),
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function logWeatherInspection(params: {
|
|
166
|
+
enabled: boolean;
|
|
167
|
+
route: string;
|
|
168
|
+
userId: string;
|
|
169
|
+
text: string;
|
|
170
|
+
inspection: WeatherMessageInspection;
|
|
171
|
+
previewChars: number;
|
|
172
|
+
}): void {
|
|
173
|
+
if (!params.enabled) {
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
logLine("weather", {
|
|
178
|
+
route: params.route,
|
|
179
|
+
userId: params.userId,
|
|
180
|
+
handled: params.inspection.shouldHandle,
|
|
181
|
+
reason: params.inspection.reason,
|
|
182
|
+
missing: params.inspection.missing,
|
|
183
|
+
location: params.inspection.location,
|
|
184
|
+
day: params.inspection.day,
|
|
185
|
+
text: previewText(params.text, params.previewChars),
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function logResponseSelection(params: {
|
|
190
|
+
enabled: boolean;
|
|
191
|
+
route: string;
|
|
192
|
+
source: "task" | "weather" | "reply";
|
|
193
|
+
finishReason: "stop" | "tool_calls";
|
|
194
|
+
matchedIntentId?: string;
|
|
195
|
+
templateId?: string;
|
|
196
|
+
text?: string | null;
|
|
197
|
+
previewChars: number;
|
|
198
|
+
}): void {
|
|
199
|
+
if (!params.enabled) {
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
logLine("response", {
|
|
204
|
+
route: params.route,
|
|
205
|
+
source: params.source,
|
|
206
|
+
finishReason: params.finishReason,
|
|
207
|
+
matchedIntentId: params.matchedIntentId,
|
|
208
|
+
templateId: params.templateId,
|
|
209
|
+
text: params.text ? previewText(params.text, params.previewChars) : undefined,
|
|
210
|
+
});
|
|
211
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { loadConfig } from "./config.js";
|
|
2
|
+
import { createApp, createAppContext } from "./server.js";
|
|
3
|
+
|
|
4
|
+
const config = loadConfig();
|
|
5
|
+
const context = await createAppContext(config);
|
|
6
|
+
const app = createApp(context);
|
|
7
|
+
|
|
8
|
+
const server = app.listen(config.port, config.host, () => {
|
|
9
|
+
console.log(
|
|
10
|
+
`[openclaw-server] listening on http://${config.host}:${config.port} using pack=${context.pack.manifest.id} model=${config.defaultModelId} debug=${config.debugLoggingEnabled ? "on" : "off"}`,
|
|
11
|
+
);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
function shutdown(signal: string): void {
|
|
15
|
+
server.close(() => {
|
|
16
|
+
context.dispose();
|
|
17
|
+
console.log(`[openclaw-server] stopped after ${signal}`);
|
|
18
|
+
process.exit(0);
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
23
|
+
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
package/src/openai.ts
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import type { EngineResult, ChatToolCall } from "./types.js";
|
|
3
|
+
|
|
4
|
+
export function createChatCompletionId(): string {
|
|
5
|
+
return `chatcmpl_${randomUUID().replace(/-/g, "")}`;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function createUsage() {
|
|
9
|
+
return {
|
|
10
|
+
prompt_tokens: 0,
|
|
11
|
+
completion_tokens: 0,
|
|
12
|
+
total_tokens: 0,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function buildTextCompletionResponse(params: {
|
|
17
|
+
id: string;
|
|
18
|
+
model: string;
|
|
19
|
+
text: string;
|
|
20
|
+
}): Record<string, unknown> {
|
|
21
|
+
return {
|
|
22
|
+
id: params.id,
|
|
23
|
+
object: "chat.completion",
|
|
24
|
+
created: Math.floor(Date.now() / 1000),
|
|
25
|
+
model: params.model,
|
|
26
|
+
choices: [
|
|
27
|
+
{
|
|
28
|
+
index: 0,
|
|
29
|
+
message: {
|
|
30
|
+
role: "assistant",
|
|
31
|
+
content: params.text,
|
|
32
|
+
},
|
|
33
|
+
finish_reason: "stop",
|
|
34
|
+
},
|
|
35
|
+
],
|
|
36
|
+
usage: createUsage(),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function buildToolCompletionResponse(params: {
|
|
41
|
+
id: string;
|
|
42
|
+
model: string;
|
|
43
|
+
toolCalls: ChatToolCall[];
|
|
44
|
+
}): Record<string, unknown> {
|
|
45
|
+
return {
|
|
46
|
+
id: params.id,
|
|
47
|
+
object: "chat.completion",
|
|
48
|
+
created: Math.floor(Date.now() / 1000),
|
|
49
|
+
model: params.model,
|
|
50
|
+
choices: [
|
|
51
|
+
{
|
|
52
|
+
index: 0,
|
|
53
|
+
message: {
|
|
54
|
+
role: "assistant",
|
|
55
|
+
content: null,
|
|
56
|
+
tool_calls: params.toolCalls,
|
|
57
|
+
},
|
|
58
|
+
finish_reason: "tool_calls",
|
|
59
|
+
},
|
|
60
|
+
],
|
|
61
|
+
usage: createUsage(),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function buildCompletionResponse(id: string, result: EngineResult): Record<string, unknown> {
|
|
66
|
+
if (result.finishReason === "tool_calls" && result.toolCalls?.length) {
|
|
67
|
+
return buildToolCompletionResponse({
|
|
68
|
+
id,
|
|
69
|
+
model: result.model,
|
|
70
|
+
toolCalls: result.toolCalls,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return buildTextCompletionResponse({
|
|
75
|
+
id,
|
|
76
|
+
model: result.model,
|
|
77
|
+
text: result.text ?? "No response available.",
|
|
78
|
+
});
|
|
79
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import type { ChatToolCall, EngineResult } from "./types.js";
|
|
3
|
+
|
|
4
|
+
type ResponseOutputItem = Record<string, unknown>;
|
|
5
|
+
|
|
6
|
+
function compactUuid(): string {
|
|
7
|
+
return randomUUID().replace(/-/g, "");
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function createUsage() {
|
|
11
|
+
return {
|
|
12
|
+
input_tokens: 0,
|
|
13
|
+
output_tokens: 0,
|
|
14
|
+
total_tokens: 0,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function createResponseId(): string {
|
|
19
|
+
return `resp_${compactUuid()}`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function createResponseOutputId(prefix: "msg" | "fc"): string {
|
|
23
|
+
return `${prefix}_${compactUuid()}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function buildResponseMessageItem(
|
|
27
|
+
text: string,
|
|
28
|
+
itemId = createResponseOutputId("msg"),
|
|
29
|
+
): ResponseOutputItem {
|
|
30
|
+
return {
|
|
31
|
+
id: itemId,
|
|
32
|
+
type: "message",
|
|
33
|
+
status: "completed",
|
|
34
|
+
role: "assistant",
|
|
35
|
+
content: [
|
|
36
|
+
{
|
|
37
|
+
type: "output_text",
|
|
38
|
+
text,
|
|
39
|
+
annotations: [],
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function buildResponseFunctionCallItem(
|
|
46
|
+
toolCall: ChatToolCall,
|
|
47
|
+
itemId = createResponseOutputId("fc"),
|
|
48
|
+
): ResponseOutputItem {
|
|
49
|
+
return {
|
|
50
|
+
id: itemId,
|
|
51
|
+
type: "function_call",
|
|
52
|
+
call_id: toolCall.id,
|
|
53
|
+
name: toolCall.function.name,
|
|
54
|
+
arguments: toolCall.function.arguments,
|
|
55
|
+
status: "completed",
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function buildResponsesInProgressResponse(params: {
|
|
60
|
+
id: string;
|
|
61
|
+
model: string;
|
|
62
|
+
createdAt?: number;
|
|
63
|
+
}): Record<string, unknown> {
|
|
64
|
+
return {
|
|
65
|
+
id: params.id,
|
|
66
|
+
object: "response",
|
|
67
|
+
created_at: params.createdAt ?? Math.floor(Date.now() / 1000),
|
|
68
|
+
status: "in_progress",
|
|
69
|
+
error: null,
|
|
70
|
+
model: params.model,
|
|
71
|
+
output: [],
|
|
72
|
+
output_text: "",
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function buildResponsesResponse(params: {
|
|
77
|
+
id: string;
|
|
78
|
+
result: EngineResult;
|
|
79
|
+
createdAt?: number;
|
|
80
|
+
outputItemId?: string;
|
|
81
|
+
}): Record<string, unknown> {
|
|
82
|
+
const createdAt = params.createdAt ?? Math.floor(Date.now() / 1000);
|
|
83
|
+
const base = {
|
|
84
|
+
id: params.id,
|
|
85
|
+
object: "response",
|
|
86
|
+
created_at: createdAt,
|
|
87
|
+
status: "completed",
|
|
88
|
+
error: null,
|
|
89
|
+
model: params.result.model,
|
|
90
|
+
usage: createUsage(),
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
if (params.result.finishReason === "tool_calls" && params.result.toolCalls?.length) {
|
|
94
|
+
return {
|
|
95
|
+
...base,
|
|
96
|
+
output: [buildResponseFunctionCallItem(params.result.toolCalls[0], params.outputItemId)],
|
|
97
|
+
output_text: "",
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const text = params.result.text ?? "No response available.";
|
|
102
|
+
return {
|
|
103
|
+
...base,
|
|
104
|
+
output: [buildResponseMessageItem(text, params.outputItemId)],
|
|
105
|
+
output_text: text,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { Express } from "express";
|
|
2
|
+
import { requireBearerAuth } from "../auth.js";
|
|
3
|
+
import type { AppContext } from "../server.js";
|
|
4
|
+
|
|
5
|
+
export function registerAdminRoutes(app: Express, context: AppContext): void {
|
|
6
|
+
app.get("/admin/stats", (req, res) => {
|
|
7
|
+
if (!requireBearerAuth(req, res, context.config)) {
|
|
8
|
+
context.recordAuthFailure();
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
res.json(context.snapshotStats());
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
app.post("/admin/packs/reload", async (req, res, next) => {
|
|
16
|
+
if (!requireBearerAuth(req, res, context.config)) {
|
|
17
|
+
context.recordAuthFailure();
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
await context.reloadPack();
|
|
23
|
+
res.json({
|
|
24
|
+
ok: true,
|
|
25
|
+
packId: context.pack.manifest.id,
|
|
26
|
+
reloadedAt: context.snapshotStats().lastReloadAt,
|
|
27
|
+
});
|
|
28
|
+
} catch (error) {
|
|
29
|
+
next(error);
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import type { Express } from "express";
|
|
2
|
+
import { requireBearerAuth } from "../auth.js";
|
|
3
|
+
import { normalizeRequest } from "../core/normalizer.js";
|
|
4
|
+
import { streamTextCompletion, streamToolCallCompletion } from "../core/stream-renderer.js";
|
|
5
|
+
import {
|
|
6
|
+
logRequestDebug,
|
|
7
|
+
logResponseSelection,
|
|
8
|
+
logTaskInspection,
|
|
9
|
+
logWeatherInspection,
|
|
10
|
+
} from "../debug-log.js";
|
|
11
|
+
import { buildCompletionResponse, createChatCompletionId } from "../openai.js";
|
|
12
|
+
import type { AppContext } from "../server.js";
|
|
13
|
+
import { buildTaskEngineResult, inspectTaskMessage } from "../tasks/chat-integration.js";
|
|
14
|
+
import { ChatCompletionsRequestSchema } from "../types.js";
|
|
15
|
+
import { inspectWeatherMessage, respondToWeatherMessage } from "../weather/chat-integration.js";
|
|
16
|
+
import { validateToolChoice } from "./request-validation.js";
|
|
17
|
+
|
|
18
|
+
export function registerChatCompletionsRoute(app: Express, context: AppContext): void {
|
|
19
|
+
app.post("/v1/chat/completions", async (req, res) => {
|
|
20
|
+
if (!requireBearerAuth(req, res, context.config)) {
|
|
21
|
+
context.recordAuthFailure();
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const parsed = ChatCompletionsRequestSchema.safeParse(req.body);
|
|
26
|
+
if (!parsed.success) {
|
|
27
|
+
const issue = parsed.error.issues[0];
|
|
28
|
+
res.status(400).json({
|
|
29
|
+
error: {
|
|
30
|
+
message: issue ? `${issue.path.join(".")}: ${issue.message}` : "Invalid request body",
|
|
31
|
+
type: "invalid_request_error",
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const toolChoiceError = validateToolChoice({
|
|
38
|
+
tools: parsed.data.tools ?? [],
|
|
39
|
+
toolChoice: parsed.data.tool_choice,
|
|
40
|
+
});
|
|
41
|
+
if (toolChoiceError) {
|
|
42
|
+
res.status(400).json({
|
|
43
|
+
error: {
|
|
44
|
+
message: toolChoiceError,
|
|
45
|
+
type: "invalid_request_error",
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const turn = normalizeRequest({
|
|
52
|
+
request: parsed.data,
|
|
53
|
+
defaultModelId: context.config.defaultModelId,
|
|
54
|
+
});
|
|
55
|
+
logRequestDebug({
|
|
56
|
+
enabled: context.config.debugLoggingEnabled,
|
|
57
|
+
route: "/v1/chat/completions",
|
|
58
|
+
body: parsed.data,
|
|
59
|
+
turn,
|
|
60
|
+
previewChars: context.config.debugPreviewChars,
|
|
61
|
+
});
|
|
62
|
+
if (!turn.userText) {
|
|
63
|
+
logRequestDebug({
|
|
64
|
+
enabled: context.config.debugLoggingEnabled,
|
|
65
|
+
route: "/v1/chat/completions",
|
|
66
|
+
body: parsed.data,
|
|
67
|
+
turn,
|
|
68
|
+
previewChars: context.config.debugPreviewChars,
|
|
69
|
+
note: "missing_user_text",
|
|
70
|
+
});
|
|
71
|
+
res.status(400).json({
|
|
72
|
+
error: {
|
|
73
|
+
message: "Missing user message in messages.",
|
|
74
|
+
type: "invalid_request_error",
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const completionId = createChatCompletionId();
|
|
81
|
+
const now = typeof req.body?.now === "string" ? req.body.now : undefined;
|
|
82
|
+
const taskMatch = inspectTaskMessage({
|
|
83
|
+
taskService: context.taskService,
|
|
84
|
+
turn,
|
|
85
|
+
explicitUser: parsed.data.user,
|
|
86
|
+
now,
|
|
87
|
+
});
|
|
88
|
+
logTaskInspection({
|
|
89
|
+
enabled: context.config.debugLoggingEnabled,
|
|
90
|
+
route: "/v1/chat/completions",
|
|
91
|
+
userId: taskMatch.userId,
|
|
92
|
+
text: turn.userText,
|
|
93
|
+
inspection: taskMatch.inspection,
|
|
94
|
+
previewChars: context.config.debugPreviewChars,
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const taskResult = taskMatch.inspection.shouldHandle
|
|
98
|
+
? buildTaskEngineResult({
|
|
99
|
+
turn,
|
|
100
|
+
taskResult: context.taskService.processMessage({
|
|
101
|
+
userId: taskMatch.userId,
|
|
102
|
+
text: turn.userText,
|
|
103
|
+
now,
|
|
104
|
+
}),
|
|
105
|
+
})
|
|
106
|
+
: undefined;
|
|
107
|
+
const weatherMatch = !taskResult
|
|
108
|
+
? inspectWeatherMessage({
|
|
109
|
+
weatherService: context.weatherService,
|
|
110
|
+
turn,
|
|
111
|
+
explicitUser: parsed.data.user,
|
|
112
|
+
})
|
|
113
|
+
: undefined;
|
|
114
|
+
if (weatherMatch) {
|
|
115
|
+
logWeatherInspection({
|
|
116
|
+
enabled: context.config.debugLoggingEnabled,
|
|
117
|
+
route: "/v1/chat/completions",
|
|
118
|
+
userId: weatherMatch.userId,
|
|
119
|
+
text: turn.userText,
|
|
120
|
+
inspection: weatherMatch.inspection,
|
|
121
|
+
previewChars: context.config.debugPreviewChars,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const weatherResult =
|
|
126
|
+
!taskResult && weatherMatch?.inspection.shouldHandle
|
|
127
|
+
? await respondToWeatherMessage({
|
|
128
|
+
weatherService: context.weatherService,
|
|
129
|
+
turn,
|
|
130
|
+
explicitUser: parsed.data.user,
|
|
131
|
+
})
|
|
132
|
+
: undefined;
|
|
133
|
+
const result = taskResult ?? weatherResult ?? context.replyEngine.respond(turn);
|
|
134
|
+
logResponseSelection({
|
|
135
|
+
enabled: context.config.debugLoggingEnabled,
|
|
136
|
+
route: "/v1/chat/completions",
|
|
137
|
+
source: taskResult ? "task" : weatherResult ? "weather" : "reply",
|
|
138
|
+
finishReason: result.finishReason,
|
|
139
|
+
matchedIntentId: result.matchedIntentId,
|
|
140
|
+
templateId: result.templateId,
|
|
141
|
+
text: result.text,
|
|
142
|
+
previewChars: context.config.debugPreviewChars,
|
|
143
|
+
});
|
|
144
|
+
context.recordCompletion({
|
|
145
|
+
stream: turn.stream,
|
|
146
|
+
finishReason: result.finishReason,
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
if (!turn.stream) {
|
|
150
|
+
res.json(buildCompletionResponse(completionId, result));
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (result.finishReason === "tool_calls" && result.toolCalls?.length) {
|
|
155
|
+
await streamToolCallCompletion({
|
|
156
|
+
res,
|
|
157
|
+
id: completionId,
|
|
158
|
+
model: result.model,
|
|
159
|
+
toolCall: result.toolCalls[0],
|
|
160
|
+
});
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
await streamTextCompletion({
|
|
165
|
+
res,
|
|
166
|
+
id: completionId,
|
|
167
|
+
model: result.model,
|
|
168
|
+
text: result.text ?? "No response available.",
|
|
169
|
+
initialDelayMs: context.config.streamInitialDelayMs,
|
|
170
|
+
chunkChars: context.config.streamChunkChars,
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
}
|