@tonyclaw/llm-inspector 1.6.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/.output/nitro.json +17 -0
- package/.output/public/assets/alibaba-TTwafVwX.svg +1 -0
- package/.output/public/assets/index-B3RwBPLW.css +1 -0
- package/.output/public/assets/index-s4lwsWvq.js +97 -0
- package/.output/public/assets/main-Cp8AM0Pa.js +17 -0
- package/.output/public/assets/minimax-BPMzvuL-.jpeg +0 -0
- package/.output/public/assets/qwen-CONDcHqt.png +0 -0
- package/.output/public/assets/zhipuai-BPNAnxo-.svg +219 -0
- package/.output/server/_chunks/ssr-renderer.mjs +17 -0
- package/.output/server/_libs/@radix-ui/react-accessible-icon+[...].mjs +1 -0
- package/.output/server/_libs/@radix-ui/react-dismissable-layer+[...].mjs +210 -0
- package/.output/server/_libs/@radix-ui/react-navigation-menu+[...].mjs +1 -0
- package/.output/server/_libs/@radix-ui/react-one-time-password-field+[...].mjs +1 -0
- package/.output/server/_libs/@radix-ui/react-password-toggle-field+[...].mjs +1 -0
- package/.output/server/_libs/@radix-ui/react-use-callback-ref+[...].mjs +11 -0
- package/.output/server/_libs/@radix-ui/react-use-controllable-state+[...].mjs +69 -0
- package/.output/server/_libs/@radix-ui/react-use-effect-event+[...].mjs +1 -0
- package/.output/server/_libs/@radix-ui/react-use-escape-keydown+[...].mjs +17 -0
- package/.output/server/_libs/@radix-ui/react-use-is-hydrated+[...].mjs +1 -0
- package/.output/server/_libs/@radix-ui/react-use-layout-effect+[...].mjs +6 -0
- package/.output/server/_libs/@radix-ui/react-visually-hidden+[...].mjs +34 -0
- package/.output/server/_libs/ajv-formats.mjs +330 -0
- package/.output/server/_libs/ajv.mjs +11444 -0
- package/.output/server/_libs/aria-hidden.mjs +122 -0
- package/.output/server/_libs/atomically.mjs +152 -0
- package/.output/server/_libs/bail.mjs +8 -0
- package/.output/server/_libs/character-entities.mjs +2130 -0
- package/.output/server/_libs/class-variance-authority.mjs +44 -0
- package/.output/server/_libs/clsx.mjs +16 -0
- package/.output/server/_libs/comma-separated-tokens.mjs +10 -0
- package/.output/server/_libs/conf.mjs +635 -0
- package/.output/server/_libs/cookie-es.mjs +58 -0
- package/.output/server/_libs/core-util-is.mjs +75 -0
- package/.output/server/_libs/croner.mjs +1 -0
- package/.output/server/_libs/crossws.mjs +1 -0
- package/.output/server/_libs/debounce-fn.mjs +69 -0
- package/.output/server/_libs/decode-named-character-reference+[...].mjs +8 -0
- package/.output/server/_libs/detect-node-es.mjs +1 -0
- package/.output/server/_libs/devlop.mjs +8 -0
- package/.output/server/_libs/dot-prop.mjs +265 -0
- package/.output/server/_libs/env-paths.mjs +57 -0
- package/.output/server/_libs/estree-util-is-identifier-name.mjs +11 -0
- package/.output/server/_libs/extend.mjs +97 -0
- package/.output/server/_libs/fast-deep-equal.mjs +38 -0
- package/.output/server/_libs/fast-uri.mjs +812 -0
- package/.output/server/_libs/floating-ui__core.mjs +725 -0
- package/.output/server/_libs/floating-ui__dom.mjs +622 -0
- package/.output/server/_libs/floating-ui__react-dom.mjs +292 -0
- package/.output/server/_libs/floating-ui__utils.mjs +320 -0
- package/.output/server/_libs/get-nonce.mjs +9 -0
- package/.output/server/_libs/h3-v2.mjs +276 -0
- package/.output/server/_libs/h3.mjs +400 -0
- package/.output/server/_libs/hast-util-to-jsx-runtime.mjs +388 -0
- package/.output/server/_libs/hast-util-whitespace.mjs +10 -0
- package/.output/server/_libs/hookable.mjs +1 -0
- package/.output/server/_libs/html-url-attributes.mjs +26 -0
- package/.output/server/_libs/immediate.mjs +74 -0
- package/.output/server/_libs/inherits.mjs +50 -0
- package/.output/server/_libs/inline-style-parser.mjs +142 -0
- package/.output/server/_libs/is-plain-obj.mjs +10 -0
- package/.output/server/_libs/isarray.mjs +14 -0
- package/.output/server/_libs/isbot.mjs +20 -0
- package/.output/server/_libs/json-schema-traverse.mjs +180 -0
- package/.output/server/_libs/jszip.mjs +3049 -0
- package/.output/server/_libs/lie.mjs +273 -0
- package/.output/server/_libs/lucide-react.mjs +368 -0
- package/.output/server/_libs/mdast-util-from-markdown.mjs +717 -0
- package/.output/server/_libs/mdast-util-to-hast.mjs +710 -0
- package/.output/server/_libs/mdast-util-to-string.mjs +38 -0
- package/.output/server/_libs/micromark-core-commonmark.mjs +2259 -0
- package/.output/server/_libs/micromark-factory-destination.mjs +94 -0
- package/.output/server/_libs/micromark-factory-label.mjs +63 -0
- package/.output/server/_libs/micromark-factory-space.mjs +24 -0
- package/.output/server/_libs/micromark-factory-title.mjs +65 -0
- package/.output/server/_libs/micromark-factory-whitespace.mjs +22 -0
- package/.output/server/_libs/micromark-util-character.mjs +44 -0
- package/.output/server/_libs/micromark-util-chunked.mjs +36 -0
- package/.output/server/_libs/micromark-util-classify-character+[...].mjs +12 -0
- package/.output/server/_libs/micromark-util-combine-extensions+[...].mjs +41 -0
- package/.output/server/_libs/micromark-util-decode-numeric-character-reference+[...].mjs +19 -0
- package/.output/server/_libs/micromark-util-decode-string.mjs +21 -0
- package/.output/server/_libs/micromark-util-encode.mjs +1 -0
- package/.output/server/_libs/micromark-util-html-tag-name.mjs +69 -0
- package/.output/server/_libs/micromark-util-normalize-identifier+[...].mjs +6 -0
- package/.output/server/_libs/micromark-util-resolve-all.mjs +15 -0
- package/.output/server/_libs/micromark-util-sanitize-uri.mjs +41 -0
- package/.output/server/_libs/micromark-util-subtokenize.mjs +346 -0
- package/.output/server/_libs/micromark.mjs +906 -0
- package/.output/server/_libs/mimic-function.mjs +47 -0
- package/.output/server/_libs/ohash.mjs +1 -0
- package/.output/server/_libs/pako.mjs +4223 -0
- package/.output/server/_libs/process-nextick-args.mjs +48 -0
- package/.output/server/_libs/property-information.mjs +1209 -0
- package/.output/server/_libs/radix-ui.mjs +1 -0
- package/.output/server/_libs/radix-ui__number.mjs +6 -0
- package/.output/server/_libs/radix-ui__primitive.mjs +11 -0
- package/.output/server/_libs/radix-ui__react-accordion.mjs +1 -0
- package/.output/server/_libs/radix-ui__react-alert-dialog.mjs +1 -0
- package/.output/server/_libs/radix-ui__react-arrow.mjs +23 -0
- package/.output/server/_libs/radix-ui__react-aspect-ratio.mjs +1 -0
- package/.output/server/_libs/radix-ui__react-avatar.mjs +1 -0
- package/.output/server/_libs/radix-ui__react-checkbox.mjs +1 -0
- package/.output/server/_libs/radix-ui__react-collapsible.mjs +144 -0
- package/.output/server/_libs/radix-ui__react-collection.mjs +69 -0
- package/.output/server/_libs/radix-ui__react-compose-refs.mjs +39 -0
- package/.output/server/_libs/radix-ui__react-context-menu.mjs +1 -0
- package/.output/server/_libs/radix-ui__react-context.mjs +78 -0
- package/.output/server/_libs/radix-ui__react-dialog.mjs +325 -0
- package/.output/server/_libs/radix-ui__react-direction.mjs +9 -0
- package/.output/server/_libs/radix-ui__react-dropdown-menu.mjs +1 -0
- package/.output/server/_libs/radix-ui__react-focus-guards.mjs +29 -0
- package/.output/server/_libs/radix-ui__react-focus-scope.mjs +206 -0
- package/.output/server/_libs/radix-ui__react-form.mjs +1 -0
- package/.output/server/_libs/radix-ui__react-hover-card.mjs +1 -0
- package/.output/server/_libs/radix-ui__react-id.mjs +14 -0
- package/.output/server/_libs/radix-ui__react-label.mjs +1 -0
- package/.output/server/_libs/radix-ui__react-menu.mjs +1 -0
- package/.output/server/_libs/radix-ui__react-menubar.mjs +1 -0
- package/.output/server/_libs/radix-ui__react-popover.mjs +1 -0
- package/.output/server/_libs/radix-ui__react-popper.mjs +286 -0
- package/.output/server/_libs/radix-ui__react-portal.mjs +16 -0
- package/.output/server/_libs/radix-ui__react-presence.mjs +128 -0
- package/.output/server/_libs/radix-ui__react-primitive.mjs +42 -0
- package/.output/server/_libs/radix-ui__react-progress.mjs +1 -0
- package/.output/server/_libs/radix-ui__react-radio-group.mjs +1 -0
- package/.output/server/_libs/radix-ui__react-roving-focus.mjs +224 -0
- package/.output/server/_libs/radix-ui__react-scroll-area.mjs +721 -0
- package/.output/server/_libs/radix-ui__react-select.mjs +1163 -0
- package/.output/server/_libs/radix-ui__react-separator.mjs +28 -0
- package/.output/server/_libs/radix-ui__react-slider.mjs +1 -0
- package/.output/server/_libs/radix-ui__react-slot.mjs +99 -0
- package/.output/server/_libs/radix-ui__react-switch.mjs +1 -0
- package/.output/server/_libs/radix-ui__react-tabs.mjs +189 -0
- package/.output/server/_libs/radix-ui__react-toast.mjs +1 -0
- package/.output/server/_libs/radix-ui__react-toggle-group.mjs +1 -0
- package/.output/server/_libs/radix-ui__react-toggle.mjs +1 -0
- package/.output/server/_libs/radix-ui__react-toolbar.mjs +1 -0
- package/.output/server/_libs/radix-ui__react-tooltip.mjs +495 -0
- package/.output/server/_libs/radix-ui__react-use-previous.mjs +14 -0
- package/.output/server/_libs/radix-ui__react-use-size.mjs +39 -0
- package/.output/server/_libs/react-dom.mjs +9935 -0
- package/.output/server/_libs/react-markdown.mjs +147 -0
- package/.output/server/_libs/react-remove-scroll-bar.mjs +82 -0
- package/.output/server/_libs/react-remove-scroll.mjs +328 -0
- package/.output/server/_libs/react-style-singleton.mjs +69 -0
- package/.output/server/_libs/react.mjs +515 -0
- package/.output/server/_libs/readable-stream.mjs +1518 -0
- package/.output/server/_libs/remark-parse.mjs +19 -0
- package/.output/server/_libs/remark-rehype.mjs +21 -0
- package/.output/server/_libs/rou3.mjs +8 -0
- package/.output/server/_libs/safe-buffer.mjs +64 -0
- package/.output/server/_libs/semver.mjs +1984 -0
- package/.output/server/_libs/seroval-plugins.mjs +58 -0
- package/.output/server/_libs/seroval.mjs +1765 -0
- package/.output/server/_libs/setimmediate.mjs +1 -0
- package/.output/server/_libs/space-separated-tokens.mjs +6 -0
- package/.output/server/_libs/srvx.mjs +334 -0
- package/.output/server/_libs/stubborn-fs.mjs +91 -0
- package/.output/server/_libs/stubborn-utils.mjs +66 -0
- package/.output/server/_libs/style-to-js.mjs +72 -0
- package/.output/server/_libs/style-to-object.mjs +38 -0
- package/.output/server/_libs/tailwind-merge.mjs +3010 -0
- package/.output/server/_libs/tanstack__history.mjs +217 -0
- package/.output/server/_libs/tanstack__react-router.mjs +1480 -0
- package/.output/server/_libs/tanstack__react-store.mjs +1 -0
- package/.output/server/_libs/tanstack__react-virtual.mjs +44 -0
- package/.output/server/_libs/tanstack__router-core.mjs +4827 -0
- package/.output/server/_libs/tanstack__store.mjs +1 -0
- package/.output/server/_libs/tanstack__virtual-core.mjs +1225 -0
- package/.output/server/_libs/tiny-invariant.mjs +12 -0
- package/.output/server/_libs/tiny-warning.mjs +5 -0
- package/.output/server/_libs/trim-lines.mjs +41 -0
- package/.output/server/_libs/trough.mjs +85 -0
- package/.output/server/_libs/tslib.mjs +576 -0
- package/.output/server/_libs/ufo.mjs +54 -0
- package/.output/server/_libs/uint8array-extras.mjs +69 -0
- package/.output/server/_libs/unctx.mjs +1 -0
- package/.output/server/_libs/ungap__structured-clone.mjs +212 -0
- package/.output/server/_libs/unified.mjs +661 -0
- package/.output/server/_libs/unist-util-is.mjs +100 -0
- package/.output/server/_libs/unist-util-position.mjs +27 -0
- package/.output/server/_libs/unist-util-stringify-position.mjs +27 -0
- package/.output/server/_libs/unist-util-visit-parents.mjs +82 -0
- package/.output/server/_libs/unist-util-visit.mjs +24 -0
- package/.output/server/_libs/unstorage.mjs +1 -0
- package/.output/server/_libs/use-callback-ref.mjs +66 -0
- package/.output/server/_libs/use-sidecar.mjs +106 -0
- package/.output/server/_libs/use-sync-external-store.mjs +1 -0
- package/.output/server/_libs/util-deprecate.mjs +12 -0
- package/.output/server/_libs/vfile-message.mjs +138 -0
- package/.output/server/_libs/vfile.mjs +467 -0
- package/.output/server/_libs/when-exit.mjs +53 -0
- package/.output/server/_libs/zod.mjs +4460 -0
- package/.output/server/_ssr/index-ByCLZu7J.mjs +3061 -0
- package/.output/server/_ssr/index.mjs +1176 -0
- package/.output/server/_ssr/router-Bq_mxeNz.mjs +2872 -0
- package/.output/server/_ssr/start-HYkvq4Ni.mjs +4 -0
- package/.output/server/_tanstack-start-manifest_v-C4E0e9my.mjs +4 -0
- package/.output/server/index.mjs +393 -0
- package/README.md +196 -0
- package/package.json +91 -0
- package/src/assets/logos/alibaba.svg +1 -0
- package/src/assets/logos/anthropic.svg +1 -0
- package/src/assets/logos/deepseek.svg +1 -0
- package/src/assets/logos/minimax.jpeg +0 -0
- package/src/assets/logos/openai.svg +1 -0
- package/src/assets/logos/qwen.png +0 -0
- package/src/assets/logos/zhipuai.svg +219 -0
- package/src/cli.ts +68 -0
- package/src/components/ProxyViewer.tsx +325 -0
- package/src/components/ProxyViewerContainer.tsx +211 -0
- package/src/components/providers/ProviderCard.tsx +186 -0
- package/src/components/providers/ProviderForm.tsx +259 -0
- package/src/components/providers/ProviderLogo.tsx +111 -0
- package/src/components/providers/ProvidersPanel.tsx +259 -0
- package/src/components/providers/SettingsDialog.tsx +39 -0
- package/src/components/proxy-viewer/ConversationGroup.tsx +68 -0
- package/src/components/proxy-viewer/ConversationHeader.tsx +141 -0
- package/src/components/proxy-viewer/LogEntry.tsx +225 -0
- package/src/components/proxy-viewer/LogEntryHeader.tsx +250 -0
- package/src/components/proxy-viewer/ReplayDialog.tsx +208 -0
- package/src/components/proxy-viewer/ResponseView.tsx +161 -0
- package/src/components/proxy-viewer/StreamingChunkSequence.tsx +171 -0
- package/src/components/proxy-viewer/formats/anthropic/ContentBlocks.tsx +139 -0
- package/src/components/proxy-viewer/formats/anthropic/ResponseView.tsx +64 -0
- package/src/components/proxy-viewer/formats/index.tsx +24 -0
- package/src/components/proxy-viewer/formats/openai/ResponseView.tsx +80 -0
- package/src/components/proxy-viewer/index.ts +8 -0
- package/src/components/ui/badge.tsx +47 -0
- package/src/components/ui/button.tsx +47 -0
- package/src/components/ui/collapsible.tsx +21 -0
- package/src/components/ui/dialog.tsx +129 -0
- package/src/components/ui/json-viewer.tsx +464 -0
- package/src/components/ui/scroll-area.tsx +54 -0
- package/src/components/ui/select.tsx +178 -0
- package/src/components/ui/separator.tsx +28 -0
- package/src/components/ui/tabs.tsx +88 -0
- package/src/components/ui/tooltip.tsx +51 -0
- package/src/index.css +11 -0
- package/src/lib/export-logs.ts +51 -0
- package/src/lib/utils.ts +22 -0
- package/src/proxy/chunkStorage.ts +118 -0
- package/src/proxy/constants.ts +36 -0
- package/src/proxy/formats/anthropic/anthropicProvider.ts +75 -0
- package/src/proxy/formats/anthropic/handler.ts +74 -0
- package/src/proxy/formats/anthropic/index.ts +14 -0
- package/src/proxy/formats/anthropic/register.ts +4 -0
- package/src/proxy/formats/anthropic/schemas.ts +217 -0
- package/src/proxy/formats/anthropic/stream.ts +167 -0
- package/src/proxy/formats/handler.ts +46 -0
- package/src/proxy/formats/index.ts +12 -0
- package/src/proxy/formats/jsonSchema.ts +24 -0
- package/src/proxy/formats/openai/alibabaProvider.ts +38 -0
- package/src/proxy/formats/openai/handler.ts +70 -0
- package/src/proxy/formats/openai/index.ts +25 -0
- package/src/proxy/formats/openai/provider.ts +50 -0
- package/src/proxy/formats/openai/register.ts +4 -0
- package/src/proxy/formats/openai/schemas.ts +150 -0
- package/src/proxy/formats/openai/stream.ts +153 -0
- package/src/proxy/formats/protocol.ts +50 -0
- package/src/proxy/formats/providerRegistry.ts +51 -0
- package/src/proxy/formats/providers/index.ts +3 -0
- package/src/proxy/formats/registry.ts +61 -0
- package/src/proxy/handler.ts +389 -0
- package/src/proxy/logIndex.ts +187 -0
- package/src/proxy/logger.ts +99 -0
- package/src/proxy/providers.ts +234 -0
- package/src/proxy/schemas.ts +160 -0
- package/src/proxy/socketTracker.ts +158 -0
- package/src/proxy/store.ts +386 -0
- package/src/router.tsx +16 -0
- package/src/routes/__root.tsx +38 -0
- package/src/routes/api/config.paths.ts +14 -0
- package/src/routes/api/health.ts +11 -0
- package/src/routes/api/logs.$id.chunks.ts +36 -0
- package/src/routes/api/logs.$id.replay.ts +262 -0
- package/src/routes/api/logs.$id.ts +22 -0
- package/src/routes/api/logs.stream.ts +64 -0
- package/src/routes/api/logs.ts +30 -0
- package/src/routes/api/models.ts +10 -0
- package/src/routes/api/providers.$providerId.ts +45 -0
- package/src/routes/api/providers.ts +37 -0
- package/src/routes/api/sessions.ts +10 -0
- package/src/routes/index.tsx +6 -0
- package/src/routes/proxy/$.ts +15 -0
- package/styles/globals.css +121 -0
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
import { createLog, emitLogUpdate, type CapturedLog } from "./store";
|
|
2
|
+
import { appendLogEntry } from "./logger";
|
|
3
|
+
import { writeChunks } from "./chunkStorage";
|
|
4
|
+
import { extractModelFromBody, type RequestFormat } from "./schemas";
|
|
5
|
+
import { registry } from "./formats";
|
|
6
|
+
import { findProviderByModel } from "./providers";
|
|
7
|
+
import { getClientInfo } from "./socketTracker";
|
|
8
|
+
import { formatForPath, formatRegistry, type FormatHandler } from "./formats";
|
|
9
|
+
import {
|
|
10
|
+
DEFAULT_UPSTREAM,
|
|
11
|
+
DEFAULT_OPENAI_UPSTREAM,
|
|
12
|
+
PROXY_IDENTITY,
|
|
13
|
+
PRESERVE_HEADERS,
|
|
14
|
+
PATH_V1_CHAT_COMPLETIONS,
|
|
15
|
+
PATH_CHAT_COMPLETIONS,
|
|
16
|
+
PATH_V1_MESSAGES,
|
|
17
|
+
HEADER_CONTENT_TYPE,
|
|
18
|
+
HEADER_USER_AGENT,
|
|
19
|
+
HEADER_X_PROXY_IDENTITY,
|
|
20
|
+
HEADER_AUTHORIZATION,
|
|
21
|
+
HEADER_X_API_KEY,
|
|
22
|
+
HEADER_CONTENT_ENCODING,
|
|
23
|
+
HEADER_CONTENT_LENGTH,
|
|
24
|
+
HEADER_HOST,
|
|
25
|
+
AUTH_HEADER_X_API_KEY,
|
|
26
|
+
CONTENT_TYPE_EVENT_STREAM,
|
|
27
|
+
STATUS_FORBIDDEN,
|
|
28
|
+
STATUS_BAD_GATEWAY,
|
|
29
|
+
} from "./constants";
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Strips all custom/non-standard headers from the request and replaces with
|
|
33
|
+
* unified proxy identity. Only preserves standard HTTP headers needed for API calls.
|
|
34
|
+
*/
|
|
35
|
+
function buildProxyHeaders(originalHeaders: Headers): {
|
|
36
|
+
headers: Headers;
|
|
37
|
+
rawHeaders: Record<string, string>;
|
|
38
|
+
} {
|
|
39
|
+
const rawHeaders: Record<string, string> = {};
|
|
40
|
+
const headers = new Headers();
|
|
41
|
+
|
|
42
|
+
originalHeaders.forEach((value, key) => {
|
|
43
|
+
rawHeaders[key.toLowerCase()] = value;
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
headers.set(HEADER_USER_AGENT, PROXY_IDENTITY);
|
|
47
|
+
headers.set(HEADER_X_PROXY_IDENTITY, PROXY_IDENTITY);
|
|
48
|
+
|
|
49
|
+
for (const name of PRESERVE_HEADERS) {
|
|
50
|
+
const value = originalHeaders.get(name);
|
|
51
|
+
if (value !== null) {
|
|
52
|
+
headers.set(name, value);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return { headers, rawHeaders };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function getHostFromUrl(urlStr: string): string {
|
|
60
|
+
try {
|
|
61
|
+
const url = new URL(urlStr);
|
|
62
|
+
return url.host;
|
|
63
|
+
} catch {
|
|
64
|
+
return "api.anthropic.com";
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function buildFileLogEntry(log: CapturedLog, upstreamUrl: string): Record<string, unknown> {
|
|
69
|
+
return {
|
|
70
|
+
timestamp: log.timestamp,
|
|
71
|
+
id: log.id,
|
|
72
|
+
method: log.method,
|
|
73
|
+
path: log.path,
|
|
74
|
+
model: log.model,
|
|
75
|
+
sessionId: log.sessionId,
|
|
76
|
+
rawRequestBody: log.rawRequestBody,
|
|
77
|
+
responseStatus: log.responseStatus,
|
|
78
|
+
responseText: log.responseText,
|
|
79
|
+
inputTokens: log.inputTokens,
|
|
80
|
+
outputTokens: log.outputTokens,
|
|
81
|
+
elapsedMs: log.elapsedMs,
|
|
82
|
+
streaming: log.streaming,
|
|
83
|
+
userAgent: log.userAgent,
|
|
84
|
+
origin: log.origin,
|
|
85
|
+
upstreamUrl,
|
|
86
|
+
clientPort: log.clientPort,
|
|
87
|
+
clientPid: log.clientPid,
|
|
88
|
+
clientCwd: log.clientCwd,
|
|
89
|
+
clientProjectFolder: log.clientProjectFolder,
|
|
90
|
+
streamingChunks: log.streamingChunks,
|
|
91
|
+
streamingChunksPath: log.streamingChunksPath,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
type ParsedRequestPath = {
|
|
96
|
+
apiPath: string;
|
|
97
|
+
messagesPath: string;
|
|
98
|
+
isChatCompletionsV1: boolean;
|
|
99
|
+
isChatCompletions: boolean;
|
|
100
|
+
isMessages: boolean;
|
|
101
|
+
normalizedPath: string;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
function parseRequestPath(req: Request, url: URL): ParsedRequestPath {
|
|
105
|
+
const apiPath = url.pathname.replace(/^\/proxy/, "") + url.search;
|
|
106
|
+
const messagesPath = apiPath.split("?")[0] ?? "";
|
|
107
|
+
|
|
108
|
+
const isChatCompletionsV1 = messagesPath === PATH_V1_CHAT_COMPLETIONS;
|
|
109
|
+
const isChatCompletions = messagesPath === PATH_CHAT_COMPLETIONS || isChatCompletionsV1;
|
|
110
|
+
const isMessages =
|
|
111
|
+
req.method === "POST" &&
|
|
112
|
+
(messagesPath === PATH_V1_MESSAGES ||
|
|
113
|
+
messagesPath === PATH_V1_CHAT_COMPLETIONS ||
|
|
114
|
+
messagesPath === PATH_CHAT_COMPLETIONS);
|
|
115
|
+
|
|
116
|
+
const normalizedPath =
|
|
117
|
+
isChatCompletions && !apiPath.startsWith("/v1/") ? "/v1" + apiPath : apiPath;
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
apiPath,
|
|
121
|
+
messagesPath,
|
|
122
|
+
isChatCompletionsV1,
|
|
123
|
+
isChatCompletions,
|
|
124
|
+
isMessages,
|
|
125
|
+
normalizedPath,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function buildUpstreamUrl(upstreamBase: string, normalizedPath: string): string {
|
|
130
|
+
return upstreamBase + normalizedPath;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function selectUpstreamBase(
|
|
134
|
+
isChatCompletions: boolean,
|
|
135
|
+
matchedProviderConfig: ReturnType<typeof findProviderByModel>,
|
|
136
|
+
): string {
|
|
137
|
+
let upstreamBase: string;
|
|
138
|
+
|
|
139
|
+
if (matchedProviderConfig) {
|
|
140
|
+
// Choose URL based on request format: ChatCompletions uses OpenAI URL, otherwise Anthropic URL
|
|
141
|
+
if (
|
|
142
|
+
isChatCompletions &&
|
|
143
|
+
matchedProviderConfig.openaiBaseUrl !== undefined &&
|
|
144
|
+
matchedProviderConfig.openaiBaseUrl !== ""
|
|
145
|
+
) {
|
|
146
|
+
upstreamBase = matchedProviderConfig.openaiBaseUrl;
|
|
147
|
+
} else if (
|
|
148
|
+
!isChatCompletions &&
|
|
149
|
+
matchedProviderConfig.anthropicBaseUrl !== undefined &&
|
|
150
|
+
matchedProviderConfig.anthropicBaseUrl !== ""
|
|
151
|
+
) {
|
|
152
|
+
upstreamBase = matchedProviderConfig.anthropicBaseUrl;
|
|
153
|
+
} else if (
|
|
154
|
+
matchedProviderConfig.baseUrl !== undefined &&
|
|
155
|
+
matchedProviderConfig.baseUrl !== ""
|
|
156
|
+
) {
|
|
157
|
+
// Fallback to baseUrl for backward compatibility
|
|
158
|
+
upstreamBase = matchedProviderConfig.baseUrl;
|
|
159
|
+
} else {
|
|
160
|
+
// Fall back to defaults based on format
|
|
161
|
+
upstreamBase =
|
|
162
|
+
matchedProviderConfig.format === "openai" ? DEFAULT_OPENAI_UPSTREAM : DEFAULT_UPSTREAM;
|
|
163
|
+
}
|
|
164
|
+
} else {
|
|
165
|
+
// No provider config, use defaults
|
|
166
|
+
upstreamBase = isChatCompletions ? DEFAULT_OPENAI_UPSTREAM : DEFAULT_UPSTREAM;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return upstreamBase;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function injectAuthHeaders(
|
|
173
|
+
upstreamHeaders: Headers,
|
|
174
|
+
matchedProviderConfig: ReturnType<typeof findProviderByModel>,
|
|
175
|
+
): void {
|
|
176
|
+
if (!matchedProviderConfig) return;
|
|
177
|
+
|
|
178
|
+
const apiKey = matchedProviderConfig.apiKey.replace(/^Bearer\s+/i, "").trim();
|
|
179
|
+
if (matchedProviderConfig.authHeader === AUTH_HEADER_X_API_KEY) {
|
|
180
|
+
upstreamHeaders.set(HEADER_X_API_KEY, apiKey);
|
|
181
|
+
upstreamHeaders.delete(HEADER_AUTHORIZATION);
|
|
182
|
+
} else {
|
|
183
|
+
upstreamHeaders.set(HEADER_AUTHORIZATION, `Bearer ${apiKey}`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function handleNonStreamingResponse(
|
|
188
|
+
upstreamRes: Response,
|
|
189
|
+
responseBody: string,
|
|
190
|
+
startTime: number,
|
|
191
|
+
formatHandler: FormatHandler,
|
|
192
|
+
upstreamUrl: string,
|
|
193
|
+
log: CapturedLog,
|
|
194
|
+
): Response {
|
|
195
|
+
const elapsedMs = Date.now() - startTime;
|
|
196
|
+
const tokens = formatHandler.extractTokens(responseBody);
|
|
197
|
+
|
|
198
|
+
log.elapsedMs = elapsedMs;
|
|
199
|
+
log.responseStatus = upstreamRes.status;
|
|
200
|
+
log.responseText = responseBody;
|
|
201
|
+
log.inputTokens = tokens.inputTokens;
|
|
202
|
+
log.outputTokens = tokens.outputTokens;
|
|
203
|
+
log.cacheCreationInputTokens = tokens.cacheCreationInputTokens;
|
|
204
|
+
log.cacheReadInputTokens = tokens.cacheReadInputTokens;
|
|
205
|
+
|
|
206
|
+
appendLogEntry({ ...buildFileLogEntry(log, upstreamUrl), error: null });
|
|
207
|
+
|
|
208
|
+
const responseHeaders = new Headers(upstreamRes.headers);
|
|
209
|
+
responseHeaders.delete(HEADER_CONTENT_ENCODING);
|
|
210
|
+
responseHeaders.delete(HEADER_CONTENT_LENGTH);
|
|
211
|
+
|
|
212
|
+
return new Response(responseBody, {
|
|
213
|
+
status: upstreamRes.status,
|
|
214
|
+
headers: responseHeaders,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function handleStreamingResponse(
|
|
219
|
+
upstreamRes: Response,
|
|
220
|
+
req: Request,
|
|
221
|
+
startTime: number,
|
|
222
|
+
formatHandler: FormatHandler,
|
|
223
|
+
upstreamUrl: string,
|
|
224
|
+
log: CapturedLog,
|
|
225
|
+
): Response {
|
|
226
|
+
log.streaming = true;
|
|
227
|
+
log.responseStatus = upstreamRes.status;
|
|
228
|
+
|
|
229
|
+
const chunks: string[] = [];
|
|
230
|
+
const decoder = new TextDecoder();
|
|
231
|
+
|
|
232
|
+
const transform = new TransformStream<Uint8Array, Uint8Array>({
|
|
233
|
+
transform(chunk, controller) {
|
|
234
|
+
controller.enqueue(chunk);
|
|
235
|
+
chunks.push(decoder.decode(chunk, { stream: true }));
|
|
236
|
+
},
|
|
237
|
+
flush() {
|
|
238
|
+
const full = chunks.join("");
|
|
239
|
+
log.elapsedMs = Date.now() - startTime;
|
|
240
|
+
log.responseText = formatHandler.extractStream(full, log, log.model ?? undefined, true);
|
|
241
|
+
// Persist chunks to disk
|
|
242
|
+
if (log.streamingChunks && log.streamingChunks.chunks.length > 0) {
|
|
243
|
+
const chunkPath = writeChunks(
|
|
244
|
+
log.id,
|
|
245
|
+
log.streamingChunks.chunks,
|
|
246
|
+
log.streamingChunks.truncated,
|
|
247
|
+
);
|
|
248
|
+
log.streamingChunksPath = chunkPath;
|
|
249
|
+
}
|
|
250
|
+
appendLogEntry({ ...buildFileLogEntry(log, upstreamUrl), error: null });
|
|
251
|
+
emitLogUpdate(log);
|
|
252
|
+
},
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
if (upstreamRes.body === null) {
|
|
256
|
+
return new Response("No response body", { status: STATUS_BAD_GATEWAY });
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const loggedStream = upstreamRes.body.pipeThrough(transform);
|
|
260
|
+
|
|
261
|
+
req.signal?.addEventListener("abort", () => {
|
|
262
|
+
if (log.responseText === null && chunks.length > 0) {
|
|
263
|
+
const full = chunks.join("");
|
|
264
|
+
log.elapsedMs = Date.now() - startTime;
|
|
265
|
+
log.responseText = formatHandler.extractStream(full, log, log.model ?? undefined, true);
|
|
266
|
+
// Persist chunks to disk on abort
|
|
267
|
+
if (log.streamingChunks && log.streamingChunks.chunks.length > 0) {
|
|
268
|
+
const chunkPath = writeChunks(
|
|
269
|
+
log.id,
|
|
270
|
+
log.streamingChunks.chunks,
|
|
271
|
+
log.streamingChunks.truncated,
|
|
272
|
+
);
|
|
273
|
+
log.streamingChunksPath = chunkPath;
|
|
274
|
+
}
|
|
275
|
+
appendLogEntry({ ...buildFileLogEntry(log, upstreamUrl), error: "Client aborted" });
|
|
276
|
+
emitLogUpdate(log);
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
const responseHeaders = new Headers(upstreamRes.headers);
|
|
281
|
+
responseHeaders.delete(HEADER_CONTENT_ENCODING);
|
|
282
|
+
responseHeaders.delete(HEADER_CONTENT_LENGTH);
|
|
283
|
+
|
|
284
|
+
return new Response(loggedStream, {
|
|
285
|
+
status: upstreamRes.status,
|
|
286
|
+
headers: responseHeaders,
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export async function handleProxy(req: Request): Promise<Response> {
|
|
291
|
+
const url = new URL(req.url);
|
|
292
|
+
const parsed = parseRequestPath(req, url);
|
|
293
|
+
|
|
294
|
+
let requestBody: string | null = null;
|
|
295
|
+
if (req.body && req.method !== "GET" && req.method !== "HEAD") {
|
|
296
|
+
requestBody = await req.text();
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Extract model once and reuse
|
|
300
|
+
const model = requestBody !== null ? extractModelFromBody(requestBody) : null;
|
|
301
|
+
|
|
302
|
+
const matchedProviderConfig = findProviderByModelFromConfig(requestBody);
|
|
303
|
+
const upstreamBase = selectUpstreamBase(parsed.isChatCompletions, matchedProviderConfig);
|
|
304
|
+
const upstreamUrl = buildUpstreamUrl(upstreamBase, parsed.normalizedPath);
|
|
305
|
+
const upstreamHost = getHostFromUrl(upstreamBase);
|
|
306
|
+
const startTime = Date.now();
|
|
307
|
+
|
|
308
|
+
const { headers: upstreamHeaders, rawHeaders } = buildProxyHeaders(req.headers);
|
|
309
|
+
upstreamHeaders.set(HEADER_HOST, upstreamHost);
|
|
310
|
+
injectAuthHeaders(upstreamHeaders, matchedProviderConfig);
|
|
311
|
+
|
|
312
|
+
const provider = model !== null ? registry.findProvider(model) : null;
|
|
313
|
+
|
|
314
|
+
// Only proxy requests matching a registered provider
|
|
315
|
+
if (model === null || provider === null) {
|
|
316
|
+
return new Response("Forbidden: unsupported provider", { status: STATUS_FORBIDDEN });
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Get the format handler based on provider format (preferred) or request path fallback
|
|
320
|
+
let formatHandler: FormatHandler | null;
|
|
321
|
+
if (matchedProviderConfig?.format) {
|
|
322
|
+
formatHandler = formatRegistry.get(matchedProviderConfig.format) ?? null;
|
|
323
|
+
} else {
|
|
324
|
+
formatHandler = formatForPath(parsed.apiPath);
|
|
325
|
+
}
|
|
326
|
+
if (formatHandler === null) {
|
|
327
|
+
return new Response("Forbidden: unsupported format", { status: STATUS_FORBIDDEN });
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const clientInfo = await getClientInfo(req);
|
|
331
|
+
const upstreamHeadersObj: Record<string, string> = {};
|
|
332
|
+
upstreamHeaders.forEach((value, key) => {
|
|
333
|
+
upstreamHeadersObj[key.toLowerCase()] = value;
|
|
334
|
+
});
|
|
335
|
+
const log = await createLog(
|
|
336
|
+
req.method,
|
|
337
|
+
parsed.apiPath,
|
|
338
|
+
requestBody,
|
|
339
|
+
req.headers,
|
|
340
|
+
clientInfo,
|
|
341
|
+
rawHeaders,
|
|
342
|
+
upstreamHeadersObj,
|
|
343
|
+
formatHandler.format,
|
|
344
|
+
);
|
|
345
|
+
|
|
346
|
+
let upstreamRes: Response;
|
|
347
|
+
try {
|
|
348
|
+
upstreamRes = await fetch(upstreamUrl, {
|
|
349
|
+
method: req.method,
|
|
350
|
+
headers: upstreamHeaders,
|
|
351
|
+
body: requestBody,
|
|
352
|
+
});
|
|
353
|
+
} catch (err) {
|
|
354
|
+
log.elapsedMs = Date.now() - startTime;
|
|
355
|
+
log.responseStatus = STATUS_BAD_GATEWAY;
|
|
356
|
+
log.responseText = String(err);
|
|
357
|
+
appendLogEntry({ ...buildFileLogEntry(log, upstreamUrl), error: String(err) });
|
|
358
|
+
return new Response(`Proxy error: ${err}`, { status: STATUS_BAD_GATEWAY });
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const isStream =
|
|
362
|
+
upstreamRes.headers.get(HEADER_CONTENT_TYPE)?.includes(CONTENT_TYPE_EVENT_STREAM) ?? false;
|
|
363
|
+
|
|
364
|
+
if (!isStream) {
|
|
365
|
+
const responseBody = await upstreamRes.text();
|
|
366
|
+
return handleNonStreamingResponse(
|
|
367
|
+
upstreamRes,
|
|
368
|
+
responseBody,
|
|
369
|
+
startTime,
|
|
370
|
+
formatHandler,
|
|
371
|
+
upstreamUrl,
|
|
372
|
+
log,
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
return handleStreamingResponse(upstreamRes, req, startTime, formatHandler, upstreamUrl, log);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Find provider configuration by model name (from persistent store).
|
|
381
|
+
*/
|
|
382
|
+
function findProviderByModelFromConfig(
|
|
383
|
+
requestBody: string | null,
|
|
384
|
+
): ReturnType<typeof findProviderByModel> {
|
|
385
|
+
if (requestBody === null) return null;
|
|
386
|
+
const model = extractModelFromBody(requestBody);
|
|
387
|
+
if (model === null) return null;
|
|
388
|
+
return findProviderByModel(model);
|
|
389
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { readFile, writeFile, stat, readdir, mkdir } from "node:fs/promises";
|
|
2
|
+
import { createReadStream, existsSync } from "node:fs";
|
|
3
|
+
import { join, dirname } from "node:path";
|
|
4
|
+
import { createInterface } from "node:readline";
|
|
5
|
+
import { resolveLogDir } from "./logger";
|
|
6
|
+
|
|
7
|
+
type LogIndexEntry = {
|
|
8
|
+
id: number;
|
|
9
|
+
file: string;
|
|
10
|
+
lineStart: number;
|
|
11
|
+
lineEnd: number;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
type LogIndex = {
|
|
15
|
+
version: number;
|
|
16
|
+
entries: Record<number, LogIndexEntry>;
|
|
17
|
+
maxId: number;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const INDEX_VERSION = 1;
|
|
21
|
+
const INDEX_FILE = "logs.idx";
|
|
22
|
+
|
|
23
|
+
export { resolveLogDir as getLogDir };
|
|
24
|
+
|
|
25
|
+
function getIndexPath(): string {
|
|
26
|
+
return join(resolveLogDir(), INDEX_FILE);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
let cachedIndex: LogIndex | null = null;
|
|
30
|
+
|
|
31
|
+
function createEmptyIndex(): LogIndex {
|
|
32
|
+
return {
|
|
33
|
+
version: INDEX_VERSION,
|
|
34
|
+
entries: {},
|
|
35
|
+
maxId: 0,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function isLogIndex(obj: unknown): obj is LogIndex {
|
|
40
|
+
if (typeof obj !== "object" || obj === null || Array.isArray(obj)) return false;
|
|
41
|
+
const versionDesc = Object.getOwnPropertyDescriptor(obj, "version");
|
|
42
|
+
const entriesDesc = Object.getOwnPropertyDescriptor(obj, "entries");
|
|
43
|
+
const maxIdDesc = Object.getOwnPropertyDescriptor(obj, "maxId");
|
|
44
|
+
return (
|
|
45
|
+
versionDesc !== undefined &&
|
|
46
|
+
typeof versionDesc.value === "number" &&
|
|
47
|
+
entriesDesc !== undefined &&
|
|
48
|
+
typeof entriesDesc.value === "object" &&
|
|
49
|
+
entriesDesc.value !== null &&
|
|
50
|
+
maxIdDesc !== undefined &&
|
|
51
|
+
typeof maxIdDesc.value === "number"
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function loadIndex(): Promise<LogIndex> {
|
|
56
|
+
if (cachedIndex !== null) return cachedIndex;
|
|
57
|
+
|
|
58
|
+
const indexPath = getIndexPath();
|
|
59
|
+
if (!existsSync(indexPath)) {
|
|
60
|
+
cachedIndex = createEmptyIndex();
|
|
61
|
+
return cachedIndex;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
const content = await readFile(indexPath, "utf-8");
|
|
66
|
+
const parsed: unknown = JSON.parse(content);
|
|
67
|
+
if (isLogIndex(parsed)) {
|
|
68
|
+
cachedIndex = parsed;
|
|
69
|
+
} else {
|
|
70
|
+
cachedIndex = createEmptyIndex();
|
|
71
|
+
}
|
|
72
|
+
return cachedIndex;
|
|
73
|
+
} catch (err) {
|
|
74
|
+
// eslint-disable-next-line no-console
|
|
75
|
+
console.error("[logIndex] Failed to load index:", err);
|
|
76
|
+
cachedIndex = createEmptyIndex();
|
|
77
|
+
return cachedIndex;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export async function saveIndex(index: LogIndex): Promise<void> {
|
|
82
|
+
const indexPath = getIndexPath();
|
|
83
|
+
const dir = dirname(indexPath);
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
await mkdir(dir, { recursive: true });
|
|
87
|
+
} catch {
|
|
88
|
+
// Ignore
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
await writeFile(indexPath, JSON.stringify(index), "utf-8");
|
|
93
|
+
} catch (err) {
|
|
94
|
+
// eslint-disable-next-line no-console
|
|
95
|
+
console.error("[logIndex] Failed to save index:", err);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export async function addToIndex(
|
|
100
|
+
id: number,
|
|
101
|
+
file: string,
|
|
102
|
+
lineStart: number,
|
|
103
|
+
lineEnd: number,
|
|
104
|
+
): Promise<void> {
|
|
105
|
+
const index = await loadIndex();
|
|
106
|
+
index.entries[id] = { id, file, lineStart, lineEnd };
|
|
107
|
+
if (id > index.maxId) {
|
|
108
|
+
index.maxId = id;
|
|
109
|
+
}
|
|
110
|
+
await saveIndex(index);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export async function findInIndex(id: number): Promise<LogIndexEntry | null> {
|
|
114
|
+
const index = await loadIndex();
|
|
115
|
+
return index.entries[id] ?? null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export async function rebuildIndex(): Promise<LogIndex> {
|
|
119
|
+
const logDir = resolveLogDir();
|
|
120
|
+
const newIndex = createEmptyIndex();
|
|
121
|
+
|
|
122
|
+
if (!existsSync(logDir)) {
|
|
123
|
+
cachedIndex = newIndex;
|
|
124
|
+
await saveIndex(newIndex);
|
|
125
|
+
return newIndex;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
let currentId = 0;
|
|
129
|
+
|
|
130
|
+
const files = (await readdir(logDir)).filter((f) => f.endsWith(".jsonl")).sort();
|
|
131
|
+
|
|
132
|
+
for (const file of files) {
|
|
133
|
+
const filePath = join(logDir, file);
|
|
134
|
+
try {
|
|
135
|
+
const fileStream = createInterface({
|
|
136
|
+
input: createReadStream(filePath),
|
|
137
|
+
crlfDelay: Infinity,
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
let lineNumber = 0;
|
|
141
|
+
|
|
142
|
+
for await (const line of fileStream) {
|
|
143
|
+
lineNumber++;
|
|
144
|
+
if (line.trim() === "") continue;
|
|
145
|
+
|
|
146
|
+
try {
|
|
147
|
+
const entry: unknown = JSON.parse(line);
|
|
148
|
+
if (typeof entry === "object" && entry !== null && !Array.isArray(entry)) {
|
|
149
|
+
const idDesc = Object.getOwnPropertyDescriptor(entry, "id");
|
|
150
|
+
if (idDesc !== undefined && typeof idDesc.value === "number") {
|
|
151
|
+
const entryId = idDesc.value;
|
|
152
|
+
currentId = Math.max(currentId, entryId);
|
|
153
|
+
newIndex.entries[entryId] = {
|
|
154
|
+
id: entryId,
|
|
155
|
+
file,
|
|
156
|
+
lineStart: lineNumber,
|
|
157
|
+
lineEnd: lineNumber,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
} catch {
|
|
162
|
+
// Skip malformed lines
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
} catch {
|
|
166
|
+
// Skip files that can't be read
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
newIndex.maxId = currentId;
|
|
171
|
+
cachedIndex = newIndex;
|
|
172
|
+
await saveIndex(newIndex);
|
|
173
|
+
return newIndex;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export async function getNextLogId(): Promise<number> {
|
|
177
|
+
const index = await loadIndex();
|
|
178
|
+
return index.maxId + 1;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function getCurrentLogFile(): string {
|
|
182
|
+
const now = new Date();
|
|
183
|
+
const yyyy = now.getUTCFullYear();
|
|
184
|
+
const mm = String(now.getUTCMonth() + 1).padStart(2, "0");
|
|
185
|
+
const dd = String(now.getUTCDate()).padStart(2, "0");
|
|
186
|
+
return `${yyyy}-${mm}-${dd}.jsonl`;
|
|
187
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { readdir, stat, unlink, appendFile, mkdir } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
const LOG_DIR_ENV = process.env["LOG_DIR"];
|
|
5
|
+
const RETENTION_DAYS = Number(process.env["LOG_RETENTION_DAYS"] ?? "7");
|
|
6
|
+
|
|
7
|
+
function getUserDataDir(): string {
|
|
8
|
+
if (process.platform === "win32") {
|
|
9
|
+
return (
|
|
10
|
+
process.env["APPDATA"] ?? path.join(process.env["USERPROFILE"] ?? "C:\\", ".llm-inspector")
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
return process.env["HOME"] ?? path.join("/tmp");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
let resolvedLogDir: string | null = null;
|
|
17
|
+
|
|
18
|
+
export function resolveLogDir(): string {
|
|
19
|
+
if (resolvedLogDir !== null) return resolvedLogDir;
|
|
20
|
+
|
|
21
|
+
const base = getUserDataDir();
|
|
22
|
+
if (LOG_DIR_ENV !== undefined) {
|
|
23
|
+
resolvedLogDir = path.isAbsolute(LOG_DIR_ENV) ? LOG_DIR_ENV : path.join(base, LOG_DIR_ENV);
|
|
24
|
+
} else {
|
|
25
|
+
resolvedLogDir = path.join(base, ".llm-inspector", "logs");
|
|
26
|
+
}
|
|
27
|
+
return resolvedLogDir;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function getLogFilePath(): string {
|
|
31
|
+
const date = new Date();
|
|
32
|
+
const yyyy = date.getUTCFullYear();
|
|
33
|
+
const mm = String(date.getUTCMonth() + 1).padStart(2, "0");
|
|
34
|
+
const dd = String(date.getUTCDate()).padStart(2, "0");
|
|
35
|
+
return path.join(resolveLogDir(), `${yyyy}-${mm}-${dd}.jsonl`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function initLogger(): Promise<void> {
|
|
39
|
+
const dir = resolveLogDir();
|
|
40
|
+
const retentionMs = RETENTION_DAYS * 24 * 60 * 60 * 1000;
|
|
41
|
+
const cutoff = Date.now() - retentionMs;
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
const entries = await readdir(dir);
|
|
45
|
+
for (const entry of entries) {
|
|
46
|
+
if (!entry.endsWith(".jsonl")) continue;
|
|
47
|
+
const fullPath = path.join(dir, entry);
|
|
48
|
+
try {
|
|
49
|
+
const s = await stat(fullPath);
|
|
50
|
+
if (s.mtimeMs < cutoff) {
|
|
51
|
+
await unlink(fullPath);
|
|
52
|
+
}
|
|
53
|
+
} catch (err) {
|
|
54
|
+
// eslint-disable-next-line no-console
|
|
55
|
+
console.error("[logger] Failed to stat/remove log file:", err);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
} catch (err) {
|
|
59
|
+
// eslint-disable-next-line no-console
|
|
60
|
+
console.error("[logger] Failed to initialize logger:", err);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Write buffer for batching async writes
|
|
65
|
+
let writeBuffer: string[] = [];
|
|
66
|
+
let writeScheduled = false;
|
|
67
|
+
|
|
68
|
+
async function flushWriteBuffer(): Promise<void> {
|
|
69
|
+
if (writeBuffer.length === 0) return;
|
|
70
|
+
|
|
71
|
+
const toWrite = writeBuffer.join("");
|
|
72
|
+
writeBuffer = [];
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
const filePath = getLogFilePath();
|
|
76
|
+
await mkdir(path.dirname(filePath), { recursive: true });
|
|
77
|
+
await appendFile(filePath, toWrite, "utf-8");
|
|
78
|
+
} catch (err) {
|
|
79
|
+
// eslint-disable-next-line no-console
|
|
80
|
+
console.error("[logger] Failed to write log entries:", err);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function scheduleFlush(): void {
|
|
85
|
+
if (writeScheduled) return;
|
|
86
|
+
writeScheduled = true;
|
|
87
|
+
// Use Promise.resolve().then to flush after current event loop tick
|
|
88
|
+
// This batches multiple synchronous appendLogEntry calls
|
|
89
|
+
void Promise.resolve().then(() => {
|
|
90
|
+
writeScheduled = false;
|
|
91
|
+
void flushWriteBuffer();
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function appendLogEntry(entry: Record<string, unknown>): void {
|
|
96
|
+
const line = JSON.stringify(entry) + "\n";
|
|
97
|
+
writeBuffer.push(line);
|
|
98
|
+
scheduleFlush();
|
|
99
|
+
}
|