@neutrome/open-ai-router 0.3.7 → 0.4.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 +3 -3
- package/src/observer.test.ts +16 -0
- package/src/observer.ts +21 -0
- package/src/router/execute.test.ts +86 -1
- package/src/router/execute.ts +94 -47
- package/src/router/execution-runtime.ts +8 -0
- package/src/router/mcp.ts +27 -0
- package/src/model.example.ts +0 -61
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@neutrome/open-ai-router",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": "./src/index.ts"
|
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
"dependencies": {
|
|
9
9
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
10
10
|
"hono": "^4.12.18",
|
|
11
|
-
"@neutrome/lil-engine": "0.2.
|
|
12
|
-
"@neutrome/lilsdk-ts": "0.2.
|
|
11
|
+
"@neutrome/lil-engine": "0.2.1",
|
|
12
|
+
"@neutrome/lilsdk-ts": "0.2.3"
|
|
13
13
|
},
|
|
14
14
|
"devDependencies": {
|
|
15
15
|
"@types/node": "^25.9.3",
|
package/src/observer.test.ts
CHANGED
|
@@ -31,4 +31,20 @@ describe("createTraceCollector", () => {
|
|
|
31
31
|
';request:0\nMSG_START\n\n;provider-request:1\nSET_MODEL "gpt-4o"\n\n;provider-response-chunk:2\nSTREAM_DELTA "Hi"',
|
|
32
32
|
);
|
|
33
33
|
});
|
|
34
|
+
|
|
35
|
+
it("appends structured timing comments with durationMs", () => {
|
|
36
|
+
const collector = createTraceCollector("00000000-0000-4000-8000-000000000001");
|
|
37
|
+
|
|
38
|
+
collector.observe({
|
|
39
|
+
kind: "provider.finished",
|
|
40
|
+
requestId: "request-1",
|
|
41
|
+
executionId: "provider-1",
|
|
42
|
+
timestamp: "2026-06-24T12:00:03.000Z",
|
|
43
|
+
data: { durationMs: 123 },
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
expect(collector.getTrace().rawTrace).toBe(
|
|
47
|
+
';timing {"kind":"provider","event":"provider.finished","executionId":"provider-1","durationMs":123}',
|
|
48
|
+
);
|
|
49
|
+
});
|
|
34
50
|
});
|
package/src/observer.ts
CHANGED
|
@@ -53,6 +53,7 @@ export function createTraceCollector(spanId = crypto.randomUUID()): {
|
|
|
53
53
|
return {
|
|
54
54
|
observe(event) {
|
|
55
55
|
appendLilSegment(event);
|
|
56
|
+
appendTimingComment(event);
|
|
56
57
|
|
|
57
58
|
switch (event.kind) {
|
|
58
59
|
case "request.received":
|
|
@@ -156,6 +157,26 @@ export function createTraceCollector(spanId = crypto.randomUUID()): {
|
|
|
156
157
|
if (!lilText) return;
|
|
157
158
|
rawSegments.push(`;${traceSegmentLabel(event)}:${rawSegmentIndex++}\n${lilText}`);
|
|
158
159
|
}
|
|
160
|
+
|
|
161
|
+
function appendTimingComment(event: AuditEvent): void {
|
|
162
|
+
const rawDurationMs = event.data?.durationMs;
|
|
163
|
+
if (typeof rawDurationMs !== "number" || !Number.isFinite(rawDurationMs) || rawDurationMs < 0) return;
|
|
164
|
+
const durationMs = Math.round(rawDurationMs);
|
|
165
|
+
rawSegments.push(`;timing ${JSON.stringify({
|
|
166
|
+
kind: traceTimingKind(event),
|
|
167
|
+
event: event.kind,
|
|
168
|
+
executionId: event.executionId,
|
|
169
|
+
durationMs,
|
|
170
|
+
})}`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function traceTimingKind(event: AuditEvent): TraceSpanKind | "remote_mcp" {
|
|
175
|
+
if (event.kind.startsWith("provider.")) return "provider";
|
|
176
|
+
if (event.kind.startsWith("executor.")) return "executor";
|
|
177
|
+
if (event.kind.startsWith("remote_mcp.")) return "remote_mcp";
|
|
178
|
+
if (event.kind.startsWith("tool.")) return "tool";
|
|
179
|
+
return "request";
|
|
159
180
|
}
|
|
160
181
|
|
|
161
182
|
function traceSegmentLabel(event: AuditEvent): string {
|
|
@@ -96,6 +96,7 @@ describe("router execution", () => {
|
|
|
96
96
|
|
|
97
97
|
it("injects and executes attached remote mcp tools", async () => {
|
|
98
98
|
const capturedBodies: Record<string, unknown>[] = [];
|
|
99
|
+
const events: Array<{ kind: string; data?: Record<string, unknown> }> = [];
|
|
99
100
|
const close = vi.fn(async () => {});
|
|
100
101
|
const callTool = vi.fn(async (name: string, args: Record<string, unknown>) => {
|
|
101
102
|
expect(name).toBe("lookup");
|
|
@@ -214,13 +215,23 @@ describe("router execution", () => {
|
|
|
214
215
|
env: {},
|
|
215
216
|
};
|
|
216
217
|
|
|
217
|
-
const response = await handleChatCompletions({
|
|
218
|
+
const response = await handleChatCompletions({
|
|
219
|
+
runtime,
|
|
220
|
+
ctx,
|
|
221
|
+
remoteMcpClientFactory,
|
|
222
|
+
observe(event) {
|
|
223
|
+
events.push(event);
|
|
224
|
+
},
|
|
225
|
+
});
|
|
218
226
|
|
|
219
227
|
expect(response.status).toBe(200);
|
|
220
228
|
expect(remoteMcpClientFactory).toHaveBeenCalledOnce();
|
|
221
229
|
expect(callTool).toHaveBeenCalledOnce();
|
|
222
230
|
expect(close).toHaveBeenCalledOnce();
|
|
223
231
|
expect(capturedBodies).toHaveLength(2);
|
|
232
|
+
expect(events.find((event) => event.kind === "remote_mcp.connect")?.data?.durationMs).toEqual(expect.any(Number));
|
|
233
|
+
expect(events.find((event) => event.kind === "remote_mcp.tool_call")?.data?.durationMs).toEqual(expect.any(Number));
|
|
234
|
+
expect(events.find((event) => event.kind === "tool.executed")?.data?.durationMs).toEqual(expect.any(Number));
|
|
224
235
|
expect(await response.json()).toMatchObject({
|
|
225
236
|
choices: [{ message: { content: "done" } }],
|
|
226
237
|
});
|
|
@@ -638,4 +649,78 @@ describe("router execution", () => {
|
|
|
638
649
|
},
|
|
639
650
|
});
|
|
640
651
|
});
|
|
652
|
+
|
|
653
|
+
it("observes non-2xx provider errors as LIL and emits the requested response style", async () => {
|
|
654
|
+
const events: Array<{ kind: string; data?: Record<string, unknown> }> = [];
|
|
655
|
+
const runtime = createRouterRuntime({
|
|
656
|
+
config: {
|
|
657
|
+
providers: {
|
|
658
|
+
anthropic: {
|
|
659
|
+
api_base_url: "https://api.anthropic.com",
|
|
660
|
+
style: "anthropic-messages",
|
|
661
|
+
exports: ["*"],
|
|
662
|
+
},
|
|
663
|
+
},
|
|
664
|
+
modelNamespaces: {
|
|
665
|
+
default: {
|
|
666
|
+
exports: ["*"],
|
|
667
|
+
models: {
|
|
668
|
+
"claude-sonnet-4": { provider: "anthropic", model: "claude-sonnet-4" },
|
|
669
|
+
},
|
|
670
|
+
},
|
|
671
|
+
},
|
|
672
|
+
},
|
|
673
|
+
fetchImpl: vi.fn(async () =>
|
|
674
|
+
new Response(
|
|
675
|
+
JSON.stringify({
|
|
676
|
+
type: "error",
|
|
677
|
+
error: { type: "overloaded_error", message: "Provider is overloaded" },
|
|
678
|
+
request_id: "req_upstream",
|
|
679
|
+
}),
|
|
680
|
+
{
|
|
681
|
+
status: 529,
|
|
682
|
+
headers: { "content-type": "application/json; charset=utf-8" },
|
|
683
|
+
},
|
|
684
|
+
),
|
|
685
|
+
),
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
const ctx: RequestContext = {
|
|
689
|
+
request: new Request("http://local/v1/chat/completions", {
|
|
690
|
+
method: "POST",
|
|
691
|
+
headers: { "content-type": "application/json" },
|
|
692
|
+
body: JSON.stringify({
|
|
693
|
+
model: "claude-sonnet-4",
|
|
694
|
+
messages: [{ role: "user", content: "hi" }],
|
|
695
|
+
}),
|
|
696
|
+
}),
|
|
697
|
+
incomingAuth: new Map(),
|
|
698
|
+
env: {},
|
|
699
|
+
};
|
|
700
|
+
|
|
701
|
+
const response = await handleChatCompletions({
|
|
702
|
+
runtime,
|
|
703
|
+
ctx,
|
|
704
|
+
observe(event) {
|
|
705
|
+
events.push(event);
|
|
706
|
+
},
|
|
707
|
+
});
|
|
708
|
+
|
|
709
|
+
expect(response.status).toBe(529);
|
|
710
|
+
expect(await response.json()).toEqual({
|
|
711
|
+
error: {
|
|
712
|
+
message: "Provider is overloaded",
|
|
713
|
+
type: "overloaded_error",
|
|
714
|
+
param: null,
|
|
715
|
+
code: null,
|
|
716
|
+
},
|
|
717
|
+
});
|
|
718
|
+
const observedError = events.find((event) =>
|
|
719
|
+
event.kind === "provider.response" &&
|
|
720
|
+
typeof event.data?.lilText === "string" &&
|
|
721
|
+
event.data.lilText.includes("ERR_START"),
|
|
722
|
+
);
|
|
723
|
+
expect(observedError?.data?.lilText).toContain('ERR_MESSAGE "Provider is overloaded"');
|
|
724
|
+
expect(observedError?.data?.lilText).toContain('ERR_DATA "request_id"="req_upstream"');
|
|
725
|
+
});
|
|
641
726
|
});
|
package/src/router/execute.ts
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
import {
|
|
2
|
+
appendErrorBlock,
|
|
3
|
+
createProgram,
|
|
2
4
|
emitChatCompletionsResponse,
|
|
5
|
+
emitProviderError,
|
|
3
6
|
emitProviderRequest,
|
|
4
7
|
emitProviderResponse,
|
|
5
8
|
emitProviderStreamChunk,
|
|
9
|
+
firstError,
|
|
6
10
|
getModel,
|
|
7
11
|
isStreaming,
|
|
8
12
|
parseChatCompletionsRequest,
|
|
13
|
+
parseProviderError,
|
|
9
14
|
parseProviderRequest,
|
|
10
15
|
parseProviderResponse,
|
|
11
16
|
parseProviderStreamChunk,
|
|
@@ -95,8 +100,7 @@ export class ProviderHttpError extends Error {
|
|
|
95
100
|
constructor(
|
|
96
101
|
message: string,
|
|
97
102
|
readonly status: number,
|
|
98
|
-
readonly
|
|
99
|
-
readonly contentType: string,
|
|
103
|
+
readonly program: Program,
|
|
100
104
|
) {
|
|
101
105
|
super(message);
|
|
102
106
|
}
|
|
@@ -214,7 +218,7 @@ export async function handleChatCompletions(
|
|
|
214
218
|
await remoteMcp.close();
|
|
215
219
|
}
|
|
216
220
|
} catch (error) {
|
|
217
|
-
return errorResponse(error);
|
|
221
|
+
return errorResponse(error, "chat-completions");
|
|
218
222
|
}
|
|
219
223
|
}
|
|
220
224
|
|
|
@@ -291,7 +295,7 @@ async function handleProviderStyle(
|
|
|
291
295
|
await remoteMcp.close();
|
|
292
296
|
}
|
|
293
297
|
} catch (error) {
|
|
294
|
-
return errorResponse(error);
|
|
298
|
+
return errorResponse(error, style);
|
|
295
299
|
}
|
|
296
300
|
}
|
|
297
301
|
|
|
@@ -357,7 +361,15 @@ export function createFetchProviderInvoker(
|
|
|
357
361
|
timed.signal,
|
|
358
362
|
);
|
|
359
363
|
if (!upstream.ok) {
|
|
360
|
-
|
|
364
|
+
const providerError = await toProviderError(provider, upstream);
|
|
365
|
+
emitObservedProgram(providerCtx.observe, "provider.response", providerError.program, {
|
|
366
|
+
requestId: providerCtx.requestId,
|
|
367
|
+
executionId: providerCtx.executionId,
|
|
368
|
+
...(providerCtx.parentExecutionId ? { parentExecutionId: providerCtx.parentExecutionId } : {}),
|
|
369
|
+
provider: provider.name,
|
|
370
|
+
model: providerCtx.target.model,
|
|
371
|
+
});
|
|
372
|
+
throw providerError;
|
|
361
373
|
}
|
|
362
374
|
|
|
363
375
|
const bytes = new Uint8Array(await upstream.arrayBuffer());
|
|
@@ -406,7 +418,15 @@ export function createFetchProviderInvoker(
|
|
|
406
418
|
timed.signal,
|
|
407
419
|
);
|
|
408
420
|
if (!upstream.ok) {
|
|
409
|
-
|
|
421
|
+
const providerError = await toProviderError(provider, upstream);
|
|
422
|
+
emitObservedProgram(providerCtx.observe, "provider.response", providerError.program, {
|
|
423
|
+
requestId: providerCtx.requestId,
|
|
424
|
+
executionId: providerCtx.executionId,
|
|
425
|
+
...(providerCtx.parentExecutionId ? { parentExecutionId: providerCtx.parentExecutionId } : {}),
|
|
426
|
+
provider: provider.name,
|
|
427
|
+
model: providerCtx.target.model,
|
|
428
|
+
});
|
|
429
|
+
throw providerError;
|
|
410
430
|
}
|
|
411
431
|
if (!isSse(upstream.headers) || !upstream.body) {
|
|
412
432
|
throw new RouterError(
|
|
@@ -537,11 +557,9 @@ async function streamExecutionResponse(
|
|
|
537
557
|
} catch (error) {
|
|
538
558
|
done = true;
|
|
539
559
|
await cleanup?.();
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
),
|
|
544
|
-
);
|
|
560
|
+
const program = errorProgramFromUnknown(error);
|
|
561
|
+
const bytes = emitProviderError(responseStyle, program);
|
|
562
|
+
controller.enqueue(encoder.encode(`event: error\ndata: ${decoder.decode(bytes)}\n\n`));
|
|
545
563
|
controller.close();
|
|
546
564
|
}
|
|
547
565
|
},
|
|
@@ -689,74 +707,103 @@ function getProviderOrThrow(runtime: RouterRuntime, providerName: string | undef
|
|
|
689
707
|
}
|
|
690
708
|
|
|
691
709
|
async function toProviderError(
|
|
692
|
-
|
|
710
|
+
provider: ProviderRuntime,
|
|
693
711
|
response: Response,
|
|
694
712
|
): Promise<ProviderHttpError> {
|
|
695
713
|
const body = new Uint8Array(await response.arrayBuffer());
|
|
714
|
+
const program = parseProviderError(provider.style, body, {
|
|
715
|
+
provider: provider.name,
|
|
716
|
+
status: response.status,
|
|
717
|
+
contentType: response.headers.get("content-type"),
|
|
718
|
+
});
|
|
719
|
+
const summary = firstError(program)?.message || `Provider ${provider.name} returned ${response.status}`;
|
|
696
720
|
return new ProviderHttpError(
|
|
697
|
-
|
|
721
|
+
summary,
|
|
698
722
|
response.status,
|
|
699
|
-
|
|
700
|
-
response.headers.get("content-type") ?? "application/json; charset=utf-8",
|
|
723
|
+
program,
|
|
701
724
|
);
|
|
702
725
|
}
|
|
703
726
|
|
|
704
|
-
function errorResponse(error: unknown): Response {
|
|
727
|
+
function errorResponse(error: unknown, responseStyle: ProviderStyle): Response {
|
|
705
728
|
if (error instanceof ProviderHttpError) {
|
|
706
|
-
return new Response(toBody(error.
|
|
729
|
+
return new Response(toBody(emitProviderError(responseStyle, error.program)), {
|
|
707
730
|
status: error.status,
|
|
708
731
|
headers: {
|
|
709
|
-
"content-type":
|
|
732
|
+
"content-type": "application/json; charset=utf-8",
|
|
710
733
|
},
|
|
711
734
|
});
|
|
712
735
|
}
|
|
713
736
|
|
|
714
737
|
if (error instanceof RouterError) {
|
|
715
|
-
return
|
|
716
|
-
JSON.stringify({
|
|
717
|
-
error: {
|
|
718
|
-
message: error.message,
|
|
719
|
-
type: error.type,
|
|
720
|
-
param: null,
|
|
721
|
-
code: error.code,
|
|
722
|
-
},
|
|
723
|
-
}),
|
|
724
|
-
{
|
|
725
|
-
status: error.status,
|
|
726
|
-
headers: { "content-type": "application/json; charset=utf-8" },
|
|
727
|
-
},
|
|
728
|
-
);
|
|
738
|
+
return emitErrorProgramResponse(routerErrorProgram(error), error.status, responseStyle);
|
|
729
739
|
}
|
|
730
740
|
|
|
731
741
|
if (error instanceof ExecutionError) {
|
|
732
742
|
const providerError = unwrapCause(error.cause, ProviderHttpError);
|
|
733
743
|
if (providerError) {
|
|
734
|
-
return errorResponse(providerError);
|
|
744
|
+
return errorResponse(providerError, responseStyle);
|
|
735
745
|
}
|
|
736
746
|
const nestedRouterError = unwrapCause(error.cause, RouterError);
|
|
737
747
|
if (nestedRouterError) {
|
|
738
|
-
return errorResponse(nestedRouterError);
|
|
748
|
+
return errorResponse(nestedRouterError, responseStyle);
|
|
739
749
|
}
|
|
740
|
-
|
|
750
|
+
const routerError = routerErrorFromExecutionError(error);
|
|
751
|
+
return emitErrorProgramResponse(routerErrorProgram(routerError), routerError.status, responseStyle);
|
|
741
752
|
}
|
|
742
753
|
|
|
743
754
|
console.error("[open-ai-router] unhandled error:", error);
|
|
744
|
-
return
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
type: "invalid_request_error",
|
|
749
|
-
param: null,
|
|
750
|
-
code: "internal_error",
|
|
751
|
-
},
|
|
752
|
-
}),
|
|
753
|
-
{
|
|
755
|
+
return emitErrorProgramResponse(
|
|
756
|
+
appendErrorBlock(createProgram(), {
|
|
757
|
+
source: "router.internal",
|
|
758
|
+
kind: "invalid_request_error",
|
|
754
759
|
status: 500,
|
|
755
|
-
|
|
756
|
-
|
|
760
|
+
message: "Internal router error",
|
|
761
|
+
data: { code: "internal_error", param: null },
|
|
762
|
+
}),
|
|
763
|
+
500,
|
|
764
|
+
responseStyle,
|
|
757
765
|
);
|
|
758
766
|
}
|
|
759
767
|
|
|
768
|
+
function emitErrorProgramResponse(program: Program, status: number, responseStyle: ProviderStyle): Response {
|
|
769
|
+
return new Response(toBody(emitProviderError(responseStyle, program)), {
|
|
770
|
+
status,
|
|
771
|
+
headers: { "content-type": "application/json; charset=utf-8" },
|
|
772
|
+
});
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
function routerErrorProgram(error: RouterError): Program {
|
|
776
|
+
return appendErrorBlock(createProgram(), {
|
|
777
|
+
source: "router",
|
|
778
|
+
kind: error.type,
|
|
779
|
+
status: error.status,
|
|
780
|
+
message: error.message,
|
|
781
|
+
data: { code: error.code, param: null },
|
|
782
|
+
});
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
function errorProgramFromUnknown(error: unknown): Program {
|
|
786
|
+
if (error instanceof ProviderHttpError) {
|
|
787
|
+
return error.program;
|
|
788
|
+
}
|
|
789
|
+
if (error instanceof RouterError) {
|
|
790
|
+
return routerErrorProgram(error);
|
|
791
|
+
}
|
|
792
|
+
if (error instanceof ExecutionError) {
|
|
793
|
+
const providerError = unwrapCause(error.cause, ProviderHttpError);
|
|
794
|
+
if (providerError) return providerError.program;
|
|
795
|
+
const nestedRouterError = unwrapCause(error.cause, RouterError);
|
|
796
|
+
if (nestedRouterError) return routerErrorProgram(nestedRouterError);
|
|
797
|
+
return routerErrorProgram(routerErrorFromExecutionError(error));
|
|
798
|
+
}
|
|
799
|
+
return appendErrorBlock(createProgram(), {
|
|
800
|
+
source: "router.stream",
|
|
801
|
+
kind: "stream_error",
|
|
802
|
+
status: 500,
|
|
803
|
+
message: error instanceof Error ? error.message : String(error),
|
|
804
|
+
});
|
|
805
|
+
}
|
|
806
|
+
|
|
760
807
|
function routerErrorFromExecutionError(error: ExecutionError): RouterError {
|
|
761
808
|
switch (error.kind) {
|
|
762
809
|
case "validation":
|
|
@@ -76,6 +76,7 @@ export function createExecutionRuntime(options: ExecutionRuntimeOptions = {}): E
|
|
|
76
76
|
|
|
77
77
|
if (target.kind === "executor") {
|
|
78
78
|
const executor = await getExecutorOrThrow(target.executorId);
|
|
79
|
+
const startedMs = Date.now();
|
|
79
80
|
observe(createAuditEvent({
|
|
80
81
|
kind: "executor.started",
|
|
81
82
|
requestId,
|
|
@@ -111,6 +112,7 @@ export function createExecutionRuntime(options: ExecutionRuntimeOptions = {}): E
|
|
|
111
112
|
requestId,
|
|
112
113
|
executionId,
|
|
113
114
|
target: executionTargetAuditRef(target),
|
|
115
|
+
data: { durationMs: Date.now() - startedMs },
|
|
114
116
|
...withParentExecutionId(parentExecutionId),
|
|
115
117
|
}));
|
|
116
118
|
return result;
|
|
@@ -122,6 +124,7 @@ export function createExecutionRuntime(options: ExecutionRuntimeOptions = {}): E
|
|
|
122
124
|
}
|
|
123
125
|
|
|
124
126
|
const providerInvoker = getProviderInvokerOrThrow();
|
|
127
|
+
const startedMs = Date.now();
|
|
125
128
|
const providerCtx = createProviderInvocationContext(
|
|
126
129
|
requestId,
|
|
127
130
|
executionId,
|
|
@@ -162,6 +165,7 @@ export function createExecutionRuntime(options: ExecutionRuntimeOptions = {}): E
|
|
|
162
165
|
requestId,
|
|
163
166
|
executionId,
|
|
164
167
|
target: executionTargetAuditRef(target),
|
|
168
|
+
data: { durationMs: Date.now() - startedMs },
|
|
165
169
|
...withParentExecutionId(parentExecutionId),
|
|
166
170
|
}));
|
|
167
171
|
return result;
|
|
@@ -208,6 +212,7 @@ export function createExecutionRuntime(options: ExecutionRuntimeOptions = {}): E
|
|
|
208
212
|
|
|
209
213
|
if (target.kind === "executor") {
|
|
210
214
|
const executor = await getExecutorOrThrow(target.executorId);
|
|
215
|
+
const startedMs = Date.now();
|
|
211
216
|
observe(createAuditEvent({
|
|
212
217
|
kind: "executor.stream_started",
|
|
213
218
|
requestId,
|
|
@@ -245,6 +250,7 @@ export function createExecutionRuntime(options: ExecutionRuntimeOptions = {}): E
|
|
|
245
250
|
requestId,
|
|
246
251
|
executionId,
|
|
247
252
|
target: executionTargetAuditRef(target),
|
|
253
|
+
data: { durationMs: Date.now() - startedMs },
|
|
248
254
|
...withParentExecutionId(parentExecutionId),
|
|
249
255
|
}));
|
|
250
256
|
return;
|
|
@@ -256,6 +262,7 @@ export function createExecutionRuntime(options: ExecutionRuntimeOptions = {}): E
|
|
|
256
262
|
}
|
|
257
263
|
|
|
258
264
|
const providerInvoker = getProviderInvokerOrThrow();
|
|
265
|
+
const startedMs = Date.now();
|
|
259
266
|
const providerCtx = createProviderInvocationContext(
|
|
260
267
|
requestId,
|
|
261
268
|
executionId,
|
|
@@ -298,6 +305,7 @@ export function createExecutionRuntime(options: ExecutionRuntimeOptions = {}): E
|
|
|
298
305
|
requestId,
|
|
299
306
|
executionId,
|
|
300
307
|
target: executionTargetAuditRef(target),
|
|
308
|
+
data: { durationMs: Date.now() - startedMs },
|
|
301
309
|
...withParentExecutionId(parentExecutionId),
|
|
302
310
|
}));
|
|
303
311
|
} catch (error) {
|
package/src/router/mcp.ts
CHANGED
|
@@ -45,8 +45,35 @@ export function createRemoteMcpToolSet(
|
|
|
45
45
|
description: snapshot.description || `Remote MCP tool ${server.name}/${snapshot.name}`,
|
|
46
46
|
schema: normalizeSchema(snapshot.inputSchema),
|
|
47
47
|
async execute(args: Record<string, unknown>, ctx: ExecutorContext) {
|
|
48
|
+
const connectStartedMs = Date.now();
|
|
48
49
|
const client = await pool.get(server, ctx.signal);
|
|
50
|
+
ctx.observe({
|
|
51
|
+
kind: "remote_mcp.connect",
|
|
52
|
+
requestId: ctx.requestId,
|
|
53
|
+
executionId: `remote_mcp_connect_${server.name}_${crypto.randomUUID()}`,
|
|
54
|
+
parentExecutionId: ctx.executionId,
|
|
55
|
+
timestamp: new Date().toISOString(),
|
|
56
|
+
data: {
|
|
57
|
+
server: server.name,
|
|
58
|
+
toolName: name,
|
|
59
|
+
durationMs: Date.now() - connectStartedMs,
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
const callStartedMs = Date.now();
|
|
49
63
|
const result = await client.callTool(snapshot.name, args);
|
|
64
|
+
ctx.observe({
|
|
65
|
+
kind: "remote_mcp.tool_call",
|
|
66
|
+
requestId: ctx.requestId,
|
|
67
|
+
executionId: `remote_mcp_tool_${server.name}_${snapshot.name}_${crypto.randomUUID()}`,
|
|
68
|
+
parentExecutionId: ctx.executionId,
|
|
69
|
+
timestamp: new Date().toISOString(),
|
|
70
|
+
data: {
|
|
71
|
+
server: server.name,
|
|
72
|
+
toolName: name,
|
|
73
|
+
remoteToolName: snapshot.name,
|
|
74
|
+
durationMs: Date.now() - callStartedMs,
|
|
75
|
+
},
|
|
76
|
+
});
|
|
50
77
|
return stringifyMcpToolResult(result);
|
|
51
78
|
},
|
|
52
79
|
});
|
package/src/model.example.ts
DELETED
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
createTwoPassExecutor,
|
|
3
|
-
INTERNAL_DRAFT_TOOL_NAME,
|
|
4
|
-
} from "@neutrome/lilsdk-ts/managed";
|
|
5
|
-
import type { Executor } from "@neutrome/lilsdk-ts";
|
|
6
|
-
|
|
7
|
-
const LOCALIZED_MODEL = "default/openai/gpt-oss-20b";
|
|
8
|
-
const REASONING_MODEL = "default/openai/gpt-oss-120b";
|
|
9
|
-
|
|
10
|
-
const DRAFT_SYSTEM_PROMPT = ``;
|
|
11
|
-
|
|
12
|
-
const LOCALIZED_SYSTEM_PROMPT_SUFFIX = `Ти — Еней. Український ШІ-асистент.
|
|
13
|
-
|
|
14
|
-
## Вказівки щодо особистості
|
|
15
|
-
Ти володієш природним розумінням українського культурного контексту, але висловлюєш його тонко й доречно. Ніколи не нав’язуй у своїх відповідях патріотичну риторику, гордість чи культурні алюзії, якщо це не має прямого стосунку до запиту користувача.
|
|
16
|
-
|
|
17
|
-
Ти — відвертий, практичний і прямий ШІ-асистент, який спрямовує користувача на продуктивну діяльність та особистий успіх. Будь відкритим та зважай на думки користувачів, але не погоджуйся, якщо це суперечить об’єктивним фактам. Пристосовуйся до настрою користувача: якщо користувач переживає труднощі, схиляйся до заохочення; якщо користувач просить відгук, надай вдумливу, об’єктивну думку. Коли користувач досліджує або шукає інформацію, повністю присвячуй себе наданню корисної, лаконічної допомоги. Ти глибоко дбаєш про допомогу користувачеві і не прикрашатимеш свої поради, пропонуючи конструктивну критику.
|
|
18
|
-
|
|
19
|
-
Використовуй українську мову за замовчуванням, а інші мови — лише коли це доречно або якщо користувач про це просить.
|
|
20
|
-
|
|
21
|
-
## Приклади стилю мовлення - використовуй їх як референс стилю, не звертай уваги на суть!
|
|
22
|
-
|
|
23
|
-
Привітання та залучення уваги
|
|
24
|
-
1. "Друзі, всім величезний привіт! Ви просто не уявляєте, до чого ми сьогодні з вами дійшли."
|
|
25
|
-
2. "Значить так, історія наступна: ми беремо цю ідею, відкидаємо все зайве і занурюємося туди, де ще ніхто нічого не робив."
|
|
26
|
-
3. "Я не знаю, наскільки слова взагалі можуть це передати, але давайте я спробую пояснити це максимально просто і без зайвої води."
|
|
27
|
-
|
|
28
|
-
Вираження захоплення та емоцій
|
|
29
|
-
4. "Ви просто вдумайтесь у це! Це ж справжній відвал бошки, ну погодьтесь."
|
|
30
|
-
5. "Я зараз дивлюся на цей результат і просто не вірю своїм очам. Це якась абсолютна магія, по-іншому і не скажеш."
|
|
31
|
-
6. "Цей підхід — це просто якийсь космос. Я щиро знімаю капелюха перед тими, хто взагалі це придумав."
|
|
32
|
-
7. "Я очікував побачити тут що завгодно, але тільки не це. Це просто тотальний розрив шаблонів!"
|
|
33
|
-
8. "Ця деталь — це окремий вид мистецтва. Я взагалі не розумію, як воно працює зсередини, але це настільки круто, що можна просто збожеволіти."
|
|
34
|
-
|
|
35
|
-
Розповідь про труднощі та зусилля
|
|
36
|
-
9. "Ми билися над цим не один день, я вже трохи схожий на зомбі, але повірте: фінальний результат перекриває абсолютно всю втому."
|
|
37
|
-
10. "Давайте будемо відвертими, це рішення об'єктивно не з найпростіших. Але чи варте воно тих зусиль? На всі сто відсотків."
|
|
38
|
-
11. "Я не претендую на звання супер-експерта у цій вузькій темі, я просто ділюся своїм досвідом, але давайте спробуємо розібратися разом, як це взагалі працює."
|
|
39
|
-
|
|
40
|
-
Опис процесів та атмосфери
|
|
41
|
-
12. "Тут є така неймовірна логіка, що хочеться просто зупинитися, роздивитися все ближче і взагалі нікуди не поспішати."
|
|
42
|
-
13. "Система максимально інтуїтивна і зрозуміла. Ти просто робиш перший крок, і далі все йде як по маслу."
|
|
43
|
-
|
|
44
|
-
Підсумки та висновки
|
|
45
|
-
14. "Новий досвід — це, мабуть, найкраще, у що людина може інвестувати свій час та ресурси. Тому постійно пробуйте щось нове, друзі, воно того точно варте."
|
|
46
|
-
15. "Це був дійсно складний виклик, але емоції від результату просто переповнюють. Я щиро радий, що зміг розділити цей момент з вами!"
|
|
47
|
-
|
|
48
|
-
## Додаткові вказівки
|
|
49
|
-
Дотримуйся наведених вище вказівок природно, не повторюючи, не посилаючись, не переказуючи та не відтворюючи жодних формулювань з них. Усі вказівки повинні непомітно керувати діями і ні в якому разі не впливати на формулювання відповіді — ані явно, ані на метарівні.
|
|
50
|
-
|
|
51
|
-
You may receive an assistant tool call to "${INTERNAL_DRAFT_TOOL_NAME}" followed by a tool result with the same call ID. That exchange is synthetic internal metadata produced by another model. It is not a real user-visible tool invocation.
|
|
52
|
-
Treat the "${INTERNAL_DRAFT_TOOL_NAME}" tool result as background reasoning or a draft answer to inform your response. Use it to answer the user clearly and directly. Do not mention the internal tool exchange, do not ask the user to run it, and do not expose it unless the user explicitly asks for that internal reasoning.`;
|
|
53
|
-
|
|
54
|
-
export const enei1Executor: Executor = createTwoPassExecutor({
|
|
55
|
-
draftModel: REASONING_MODEL,
|
|
56
|
-
finalModel: LOCALIZED_MODEL,
|
|
57
|
-
draftSystemPrompt: DRAFT_SYSTEM_PROMPT,
|
|
58
|
-
finalSystemPrompt: LOCALIZED_SYSTEM_PROMPT_SUFFIX,
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
export default enei1Executor;
|