@neutrome/open-ai-router 0.1.8 → 0.1.10

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.8",
3
+ "version": "0.1.10",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./src/index.ts"
@@ -14,7 +14,7 @@ import {
14
14
  } from "../router/index.ts";
15
15
  import type { RequestContext } from "../auth/types.ts";
16
16
  import type { RouterConfig } from "../router/config.ts";
17
- import type { UsageReport } from "../router/execute.ts";
17
+ import type { UsageReport, ProviderCallReport } from "../router/execute.ts";
18
18
 
19
19
  export type RouterAppAuth = {
20
20
  authenticate(request: Request, env: Record<string, unknown>): Promise<object | null>;
@@ -24,6 +24,7 @@ export type RouterAppAuth = {
24
24
  export type RouterAppHooks = {
25
25
  beforeRequest?(ctx: { request: Request; model: string | null; env: Record<string, unknown> }): Promise<Response | void>;
26
26
  onUsage?(report: UsageReport, env: Record<string, unknown>): void;
27
+ onProviderCall?(report: ProviderCallReport, env: Record<string, unknown>): void;
27
28
  onToolCall?(report: { toolName: string }, env: Record<string, unknown>): void;
28
29
  };
29
30
 
@@ -115,6 +116,9 @@ export function createRouterApp(options: RouterAppOptions) {
115
116
  const onUsage = options.hooks?.onUsage
116
117
  ? (report: UsageReport) => options.hooks!.onUsage!(report, env)
117
118
  : undefined;
119
+ const onProviderCall = options.hooks?.onProviderCall
120
+ ? (report: ProviderCallReport) => options.hooks!.onProviderCall!(report, env)
121
+ : undefined;
118
122
 
119
123
  return handler({
120
124
  runtime,
@@ -124,6 +128,7 @@ export function createRouterApp(options: RouterAppOptions) {
124
128
  ...(options.observe ? { observe: options.observe } : {}),
125
129
  ...(options.upstreamTimeoutMs !== undefined ? { upstreamTimeoutMs: options.upstreamTimeoutMs } : {}),
126
130
  ...(onUsage ? { onUsage } : {}),
131
+ ...(onProviderCall ? { onProviderCall } : {}),
127
132
  ...(forceStreaming ? { forceStreaming: true } : {}),
128
133
  });
129
134
  }
package/src/index.ts CHANGED
@@ -32,6 +32,8 @@ export type {
32
32
  HandleResponsesOptions,
33
33
  ProviderApiStyle,
34
34
  ProviderApiStyle as ProviderStyle,
35
+ ProviderCallReport,
36
+ ProviderErrorReport,
35
37
  ProviderRuntime,
36
38
  ProviderTargetConfig,
37
39
  ResolvedTarget,
@@ -36,11 +36,23 @@ const DEFAULT_UPSTREAM_TIMEOUT_MS = 120_000;
36
36
 
37
37
  export type UsageReport = {
38
38
  model: string;
39
+ provider: string;
39
40
  tokensInput: number;
40
41
  tokensOutput: number;
41
42
  cachedTokensInput: number;
42
43
  };
43
44
 
45
+ export type ProviderErrorReport = {
46
+ model: string;
47
+ provider: string;
48
+ status: number;
49
+ message: string;
50
+ };
51
+
52
+ export type ProviderCallReport =
53
+ | { kind: "usage"; } & UsageReport
54
+ | { kind: "error"; } & ProviderErrorReport;
55
+
44
56
  export type RouterExecutionOptions = Pick<
45
57
  ExecutionRuntimeOptions,
46
58
  "observe" | "requestIdFactory" | "executionIdFactory" | "resolveExecutor"
@@ -49,6 +61,7 @@ export type RouterExecutionOptions = Pick<
49
61
  transforms?: Readonly<Record<string, ProgramTransform>>;
50
62
  upstreamTimeoutMs?: number;
51
63
  onUsage?: (report: UsageReport) => void;
64
+ onProviderCall?: (report: ProviderCallReport) => void;
52
65
  };
53
66
 
54
67
  export type HandleChatCompletionsOptions = RouterExecutionOptions & {
@@ -108,6 +121,7 @@ export function createRouterExecutionRuntime(
108
121
  runtime,
109
122
  ctx,
110
123
  options.upstreamTimeoutMs,
124
+ options.onProviderCall,
111
125
  ),
112
126
  };
113
127
 
@@ -297,6 +311,7 @@ export function createFetchProviderInvoker(
297
311
  runtime: RouterRuntime,
298
312
  ctx: RequestContext,
299
313
  upstreamTimeoutMs = DEFAULT_UPSTREAM_TIMEOUT_MS,
314
+ onProviderCall?: (report: ProviderCallReport) => void,
300
315
  ): ProviderInvoker {
301
316
  return {
302
317
  async execute(request, providerCtx) {
@@ -312,11 +327,15 @@ export function createFetchProviderInvoker(
312
327
  timed.signal,
313
328
  );
314
329
  if (!upstream.ok) {
315
- throw await toProviderError(provider.name, upstream);
330
+ const err = await toProviderError(provider.name, upstream);
331
+ reportProviderError(onProviderCall, providerCtx.target, err);
332
+ throw err;
316
333
  }
317
334
 
318
335
  const bytes = new Uint8Array(await upstream.arrayBuffer());
319
- return parseProviderResponse(provider.style, bytes);
336
+ const response = parseProviderResponse(provider.style, bytes);
337
+ reportProviderUsage(onProviderCall, response, providerCtx.target);
338
+ return response;
320
339
  } catch (error) {
321
340
  throw normalizeUpstreamAbort(error, timed.signal);
322
341
  } finally {
@@ -339,7 +358,9 @@ export function createFetchProviderInvoker(
339
358
  timed.signal,
340
359
  );
341
360
  if (!upstream.ok) {
342
- throw await toProviderError(provider.name, upstream);
361
+ const err = await toProviderError(provider.name, upstream);
362
+ reportProviderError(onProviderCall, providerCtx.target, err);
363
+ throw err;
343
364
  }
344
365
  if (!isSse(upstream.headers) || !upstream.body) {
345
366
  throw new RouterError(
@@ -350,6 +371,7 @@ export function createFetchProviderInvoker(
350
371
  }
351
372
 
352
373
  reader = upstream.body.getReader();
374
+ let lastChunkWithUsage: Program | null = null;
353
375
  while (true) {
354
376
  const { done, value } = await reader.read();
355
377
  if (done) {
@@ -361,13 +383,16 @@ export function createFetchProviderInvoker(
361
383
  for (const event of split.events) {
362
384
  for (const data of eventDataLines(event)) {
363
385
  if (data === "[DONE]") {
386
+ reportProviderUsage(onProviderCall, lastChunkWithUsage, providerCtx.target);
364
387
  return;
365
388
  }
366
389
  if (data[0] !== "{") continue;
367
- yield parseProviderStreamChunk(
390
+ const chunk = parseProviderStreamChunk(
368
391
  provider.style,
369
392
  encoder.encode(data),
370
393
  );
394
+ if (usageObject(chunk) !== undefined) lastChunkWithUsage = chunk;
395
+ yield chunk;
371
396
  }
372
397
  }
373
398
  }
@@ -375,15 +400,19 @@ export function createFetchProviderInvoker(
375
400
  if (buffer.length > 0) {
376
401
  for (const data of eventDataLines(buffer)) {
377
402
  if (data === "[DONE]") {
403
+ reportProviderUsage(onProviderCall, lastChunkWithUsage, providerCtx.target);
378
404
  return;
379
405
  }
380
406
  if (data[0] !== "{") continue;
381
- yield parseProviderStreamChunk(
407
+ const chunk = parseProviderStreamChunk(
382
408
  provider.style,
383
409
  encoder.encode(data),
384
410
  );
411
+ if (usageObject(chunk) !== undefined) lastChunkWithUsage = chunk;
412
+ yield chunk;
385
413
  }
386
414
  }
415
+ reportProviderUsage(onProviderCall, lastChunkWithUsage, providerCtx.target);
387
416
  } catch (error) {
388
417
  throw normalizeUpstreamAbort(error, timed.signal);
389
418
  } finally {
@@ -727,8 +756,55 @@ function reportUsage(
727
756
  const details = u.prompt_tokens_details as Record<string, unknown> | undefined;
728
757
  const cachedTokensInput = details ? asNum(details.cached_tokens) : 0;
729
758
  const model = resolved.target.kind === "provider" ? resolved.target.model : resolved.target.alias;
759
+ const provider = resolved.target.kind === "provider" ? (resolved.target.provider ?? "") : "";
760
+ try {
761
+ onUsage({ model, provider, tokensInput, tokensOutput, cachedTokensInput });
762
+ } catch {}
763
+ }
764
+
765
+ function reportProviderUsage(
766
+ onProviderCall: ((report: ProviderCallReport) => void) | undefined,
767
+ response: Program | null,
768
+ target: Extract<import("@neutrome/lil-engine").ExecutionTarget, { kind: "provider" }>,
769
+ ): void {
770
+ if (!onProviderCall) return;
771
+ const usage = response ? usageObject(response) : undefined;
772
+ const u = (usage && typeof usage === "object" ? usage : {}) as Record<string, unknown>;
773
+ const tokensInput = asNum(u.prompt_tokens) || asNum(u.input_tokens);
774
+ const tokensOutput = asNum(u.completion_tokens) || asNum(u.output_tokens);
775
+ const details = u.prompt_tokens_details as Record<string, unknown> | undefined;
776
+ const cachedTokensInput = details ? asNum(details.cached_tokens) : 0;
777
+ try {
778
+ onProviderCall({
779
+ kind: "usage",
780
+ model: target.model,
781
+ provider: target.provider ?? "",
782
+ tokensInput,
783
+ tokensOutput,
784
+ cachedTokensInput,
785
+ });
786
+ } catch {}
787
+ }
788
+
789
+ function reportProviderError(
790
+ onProviderCall: ((report: ProviderCallReport) => void) | undefined,
791
+ target: Extract<import("@neutrome/lil-engine").ExecutionTarget, { kind: "provider" }>,
792
+ error: ProviderHttpError,
793
+ ): void {
794
+ if (!onProviderCall) return;
795
+ let message = `HTTP ${error.status}`;
796
+ try {
797
+ const text = decoder.decode(error.body).slice(0, 500);
798
+ message = text || message;
799
+ } catch {}
730
800
  try {
731
- onUsage({ model, tokensInput, tokensOutput, cachedTokensInput });
801
+ onProviderCall({
802
+ kind: "error",
803
+ model: target.model,
804
+ provider: target.provider ?? "",
805
+ status: error.status,
806
+ message,
807
+ });
732
808
  } catch {}
733
809
  }
734
810
 
@@ -33,5 +33,7 @@ export type {
33
33
  HandleResponsesOptions,
34
34
  RouterExecutionOptions,
35
35
  UsageReport,
36
+ ProviderCallReport,
37
+ ProviderErrorReport,
36
38
  } from "./execute.ts";
37
39
  export type { ResolvedTarget } from "./resolve.ts";