@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neutrome/open-ai-router",
3
- "version": "0.1.12",
3
+ "version": "0.1.13",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./src/index.ts"
@@ -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
+ }
@@ -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
- Promise.resolve(options.hooks?.onTrace?.(collector.getTrace(), env)).catch((error) => {
124
- console.error("[open-ai-router] onTrace hook failed:", error);
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>({