@neutrome/open-ai-router 0.1.12 → 0.1.13
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 +1 -1
- package/src/app/factory.test.ts +156 -0
- package/src/app/factory.ts +21 -3
package/package.json
CHANGED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import type { RouterConfig } from "../router/index.ts";
|
|
3
|
+
import { createRouterApp } from "./factory.ts";
|
|
4
|
+
|
|
5
|
+
const encoder = new TextEncoder();
|
|
6
|
+
|
|
7
|
+
const config: RouterConfig = {
|
|
8
|
+
providers: {
|
|
9
|
+
openrouter: {
|
|
10
|
+
api_base_url: "https://openrouter.ai/api/v1",
|
|
11
|
+
style: "chat-completions",
|
|
12
|
+
exports: ["*"],
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
executors: {
|
|
16
|
+
default: {
|
|
17
|
+
exports: ["*"],
|
|
18
|
+
models: {
|
|
19
|
+
"gpt-4o": { provider: "openrouter", model: "gpt-4o" },
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
describe("createRouterApp trace finalization", () => {
|
|
26
|
+
it("keeps non-stream trace hooks alive with waitUntil", async () => {
|
|
27
|
+
const waited: Promise<unknown>[] = [];
|
|
28
|
+
const onTrace = vi.fn(async () => {});
|
|
29
|
+
const app = createRouterApp({
|
|
30
|
+
config,
|
|
31
|
+
executors: {},
|
|
32
|
+
auth: {
|
|
33
|
+
async authenticate() {
|
|
34
|
+
return {};
|
|
35
|
+
},
|
|
36
|
+
canAccess() {
|
|
37
|
+
return true;
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
hooks: { onTrace },
|
|
41
|
+
fetchImpl: vi.fn(async () =>
|
|
42
|
+
new Response(
|
|
43
|
+
JSON.stringify({
|
|
44
|
+
id: "resp_1",
|
|
45
|
+
model: "gpt-4o",
|
|
46
|
+
choices: [
|
|
47
|
+
{
|
|
48
|
+
index: 0,
|
|
49
|
+
message: { role: "assistant", content: "hello" },
|
|
50
|
+
finish_reason: "stop",
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
}),
|
|
54
|
+
{ headers: { "content-type": "application/json; charset=utf-8" } },
|
|
55
|
+
)),
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const response = await app.fetch(
|
|
59
|
+
new Request("http://local/v1/chat/completions", {
|
|
60
|
+
method: "POST",
|
|
61
|
+
headers: { "content-type": "application/json" },
|
|
62
|
+
body: JSON.stringify({
|
|
63
|
+
model: "gpt-4o",
|
|
64
|
+
messages: [{ role: "user", content: "hi" }],
|
|
65
|
+
}),
|
|
66
|
+
}),
|
|
67
|
+
{},
|
|
68
|
+
createExecutionContext(waited) as never,
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
expect(response.status).toBe(200);
|
|
72
|
+
expect(waited).toHaveLength(1);
|
|
73
|
+
await waited[0];
|
|
74
|
+
expect(onTrace).toHaveBeenCalledTimes(1);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("keeps stream trace hooks alive until the stream closes", async () => {
|
|
78
|
+
const waited: Promise<unknown>[] = [];
|
|
79
|
+
const onTrace = vi.fn(async () => {});
|
|
80
|
+
const app = createRouterApp({
|
|
81
|
+
config,
|
|
82
|
+
executors: {},
|
|
83
|
+
auth: {
|
|
84
|
+
async authenticate() {
|
|
85
|
+
return {};
|
|
86
|
+
},
|
|
87
|
+
canAccess() {
|
|
88
|
+
return true;
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
hooks: { onTrace },
|
|
92
|
+
fetchImpl: vi.fn(async () => {
|
|
93
|
+
const stream = new ReadableStream<Uint8Array>({
|
|
94
|
+
start(controller) {
|
|
95
|
+
controller.enqueue(
|
|
96
|
+
encoder.encode(
|
|
97
|
+
`data: ${JSON.stringify({
|
|
98
|
+
id: "chunk_1",
|
|
99
|
+
model: "gpt-4o",
|
|
100
|
+
choices: [{ index: 0, delta: { role: "assistant", content: "Hi" } }],
|
|
101
|
+
})}\n\n`,
|
|
102
|
+
),
|
|
103
|
+
);
|
|
104
|
+
controller.enqueue(
|
|
105
|
+
encoder.encode(
|
|
106
|
+
`data: ${JSON.stringify({
|
|
107
|
+
id: "chunk_1",
|
|
108
|
+
model: "gpt-4o",
|
|
109
|
+
choices: [{ index: 0, delta: {}, finish_reason: "stop" }],
|
|
110
|
+
})}\n\n`,
|
|
111
|
+
),
|
|
112
|
+
);
|
|
113
|
+
controller.enqueue(encoder.encode("data: [DONE]\n\n"));
|
|
114
|
+
controller.close();
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
return new Response(stream, {
|
|
119
|
+
headers: { "content-type": "text/event-stream; charset=utf-8" },
|
|
120
|
+
});
|
|
121
|
+
}),
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
const response = await app.fetch(
|
|
125
|
+
new Request("http://local/v1/chat/completions", {
|
|
126
|
+
method: "POST",
|
|
127
|
+
headers: { "content-type": "application/json" },
|
|
128
|
+
body: JSON.stringify({
|
|
129
|
+
model: "gpt-4o",
|
|
130
|
+
stream: true,
|
|
131
|
+
messages: [{ role: "user", content: "hi" }],
|
|
132
|
+
}),
|
|
133
|
+
}),
|
|
134
|
+
{},
|
|
135
|
+
createExecutionContext(waited) as never,
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
expect(response.headers.get("content-type")).toBe("text/event-stream; charset=utf-8");
|
|
139
|
+
expect(waited).toHaveLength(0);
|
|
140
|
+
|
|
141
|
+
await response.text();
|
|
142
|
+
|
|
143
|
+
expect(waited).toHaveLength(1);
|
|
144
|
+
await waited[0];
|
|
145
|
+
expect(onTrace).toHaveBeenCalledTimes(1);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
function createExecutionContext(waited: Promise<unknown>[]) {
|
|
150
|
+
return {
|
|
151
|
+
waitUntil(promise: Promise<unknown>) {
|
|
152
|
+
waited.push(promise);
|
|
153
|
+
},
|
|
154
|
+
passThroughOnException() {},
|
|
155
|
+
};
|
|
156
|
+
}
|
package/src/app/factory.ts
CHANGED
|
@@ -112,6 +112,7 @@ export function createRouterApp(options: RouterAppOptions) {
|
|
|
112
112
|
}
|
|
113
113
|
|
|
114
114
|
const collector = createTraceCollector();
|
|
115
|
+
const onTrace = options.hooks?.onTrace;
|
|
115
116
|
const observe = (event: Parameters<NonNullable<ExecutionRuntimeOptions["observe"]>>[0]) => {
|
|
116
117
|
collector.observe(event);
|
|
117
118
|
options.observe?.(event);
|
|
@@ -120,9 +121,12 @@ export function createRouterApp(options: RouterAppOptions) {
|
|
|
120
121
|
const finalizeTrace = () => {
|
|
121
122
|
if (traceFinalized) return;
|
|
122
123
|
traceFinalized = true;
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
124
|
+
if (!onTrace) return;
|
|
125
|
+
runBackgroundTask(
|
|
126
|
+
c,
|
|
127
|
+
() => onTrace(collector.getTrace(), env),
|
|
128
|
+
"onTrace hook",
|
|
129
|
+
);
|
|
126
130
|
};
|
|
127
131
|
|
|
128
132
|
try {
|
|
@@ -208,6 +212,20 @@ function isEventStreamResponse(response: Response): boolean {
|
|
|
208
212
|
return contentType.startsWith("text/event-stream");
|
|
209
213
|
}
|
|
210
214
|
|
|
215
|
+
function runBackgroundTask(c: Context, task: () => unknown, label: string): void {
|
|
216
|
+
const tracked = Promise.resolve()
|
|
217
|
+
.then(task)
|
|
218
|
+
.catch((error) => {
|
|
219
|
+
console.error(`[open-ai-router] ${label} failed:`, error);
|
|
220
|
+
});
|
|
221
|
+
const executionCtx = c.executionCtx as { waitUntil?(promise: Promise<unknown>): void } | undefined;
|
|
222
|
+
if (executionCtx?.waitUntil) {
|
|
223
|
+
executionCtx.waitUntil(tracked);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
void tracked;
|
|
227
|
+
}
|
|
228
|
+
|
|
211
229
|
function wrapResponseWithFinalize(response: Response, finalize: () => void): Response {
|
|
212
230
|
const reader = response.body!.getReader();
|
|
213
231
|
const body = new ReadableStream<Uint8Array>({
|