cc-inspector 0.1.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/README.md +40 -0
- package/package.json +77 -0
- package/src/App.tsx +7 -0
- package/src/components/ProxyViewer.tsx +102 -0
- package/src/components/ProxyViewerContainer.tsx +48 -0
- package/src/components/proxy-viewer/LogEntry.tsx +95 -0
- package/src/components/proxy-viewer/LogEntryHeader.tsx +148 -0
- package/src/components/proxy-viewer/MessageThread.tsx +55 -0
- package/src/components/proxy-viewer/RequestView.tsx +77 -0
- package/src/components/proxy-viewer/ResponseView.tsx +195 -0
- package/src/components/proxy-viewer/SystemPrompt.tsx +65 -0
- package/src/components/proxy-viewer/ToolDefinitions.tsx +82 -0
- package/src/components/proxy-viewer/content-blocks.tsx +288 -0
- package/src/components/ui/badge.tsx +47 -0
- package/src/components/ui/collapsible.tsx +21 -0
- package/src/components/ui/json-viewer.tsx +383 -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/frontend.tsx +26 -0
- package/src/index.css +11 -0
- package/src/index.html +12 -0
- package/src/index.ts +56 -0
- package/src/lib/utils.ts +6 -0
- package/src/proxy/handler.ts +146 -0
- package/src/proxy/schemas.ts +241 -0
- package/src/proxy/store.ts +73 -0
- package/styles/globals.css +121 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
4
|
+
import { Tabs as TabsPrimitive } from "radix-ui";
|
|
5
|
+
import * as React from "react";
|
|
6
|
+
|
|
7
|
+
import { cn } from "@/lib/utils";
|
|
8
|
+
|
|
9
|
+
function Tabs({
|
|
10
|
+
className,
|
|
11
|
+
orientation = "horizontal",
|
|
12
|
+
...props
|
|
13
|
+
}: React.ComponentProps<typeof TabsPrimitive.Root>): React.JSX.Element {
|
|
14
|
+
return (
|
|
15
|
+
<TabsPrimitive.Root
|
|
16
|
+
data-slot="tabs"
|
|
17
|
+
data-orientation={orientation}
|
|
18
|
+
orientation={orientation}
|
|
19
|
+
className={cn("group/tabs flex gap-2 data-[orientation=horizontal]:flex-col", className)}
|
|
20
|
+
{...props}
|
|
21
|
+
/>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const tabsListVariants = cva(
|
|
26
|
+
"rounded-lg p-[3px] group-data-[orientation=horizontal]/tabs:h-9 data-[variant=line]:rounded-none group/tabs-list text-muted-foreground inline-flex w-fit items-center justify-center group-data-[orientation=vertical]/tabs:h-fit group-data-[orientation=vertical]/tabs:flex-col",
|
|
27
|
+
{
|
|
28
|
+
variants: {
|
|
29
|
+
variant: {
|
|
30
|
+
default: "bg-muted",
|
|
31
|
+
line: "gap-1 bg-transparent",
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
defaultVariants: {
|
|
35
|
+
variant: "default",
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
function TabsList({
|
|
41
|
+
className,
|
|
42
|
+
variant = "default",
|
|
43
|
+
...props
|
|
44
|
+
}: React.ComponentProps<typeof TabsPrimitive.List> &
|
|
45
|
+
VariantProps<typeof tabsListVariants>): React.JSX.Element {
|
|
46
|
+
return (
|
|
47
|
+
<TabsPrimitive.List
|
|
48
|
+
data-slot="tabs-list"
|
|
49
|
+
data-variant={variant}
|
|
50
|
+
className={cn(tabsListVariants({ variant }), className)}
|
|
51
|
+
{...props}
|
|
52
|
+
/>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function TabsTrigger({
|
|
57
|
+
className,
|
|
58
|
+
...props
|
|
59
|
+
}: React.ComponentProps<typeof TabsPrimitive.Trigger>): React.JSX.Element {
|
|
60
|
+
return (
|
|
61
|
+
<TabsPrimitive.Trigger
|
|
62
|
+
data-slot="tabs-trigger"
|
|
63
|
+
className={cn(
|
|
64
|
+
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring text-foreground/60 hover:text-foreground dark:text-muted-foreground dark:hover:text-foreground relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-all group-data-[orientation=vertical]/tabs:w-full group-data-[orientation=vertical]/tabs:justify-start focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 group-data-[variant=default]/tabs-list:data-[state=active]:shadow-sm group-data-[variant=line]/tabs-list:data-[state=active]:shadow-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
65
|
+
"group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:border-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent",
|
|
66
|
+
"data-[state=active]:bg-background dark:data-[state=active]:text-foreground dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 data-[state=active]:text-foreground",
|
|
67
|
+
"after:bg-foreground after:absolute after:opacity-0 after:transition-opacity group-data-[orientation=horizontal]/tabs:after:inset-x-0 group-data-[orientation=horizontal]/tabs:after:bottom-[-5px] group-data-[orientation=horizontal]/tabs:after:h-0.5 group-data-[orientation=vertical]/tabs:after:inset-y-0 group-data-[orientation=vertical]/tabs:after:-right-1 group-data-[orientation=vertical]/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-[state=active]:after:opacity-100",
|
|
68
|
+
className,
|
|
69
|
+
)}
|
|
70
|
+
{...props}
|
|
71
|
+
/>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function TabsContent({
|
|
76
|
+
className,
|
|
77
|
+
...props
|
|
78
|
+
}: React.ComponentProps<typeof TabsPrimitive.Content>): React.JSX.Element {
|
|
79
|
+
return (
|
|
80
|
+
<TabsPrimitive.Content
|
|
81
|
+
data-slot="tabs-content"
|
|
82
|
+
className={cn("flex-1 outline-none", className)}
|
|
83
|
+
{...props}
|
|
84
|
+
/>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants };
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { Tooltip as TooltipPrimitive } from "radix-ui";
|
|
3
|
+
|
|
4
|
+
import { cn } from "@/lib/utils";
|
|
5
|
+
|
|
6
|
+
function TooltipProvider({
|
|
7
|
+
delayDuration = 0,
|
|
8
|
+
...props
|
|
9
|
+
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
|
|
10
|
+
return (
|
|
11
|
+
<TooltipPrimitive.Provider
|
|
12
|
+
data-slot="tooltip-provider"
|
|
13
|
+
delayDuration={delayDuration}
|
|
14
|
+
{...props}
|
|
15
|
+
/>
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function Tooltip({ ...props }: React.ComponentProps<typeof TooltipPrimitive.Root>) {
|
|
20
|
+
return <TooltipPrimitive.Root data-slot="tooltip" {...props} />;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function TooltipTrigger({ ...props }: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
|
|
24
|
+
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function TooltipContent({
|
|
28
|
+
className,
|
|
29
|
+
sideOffset = 0,
|
|
30
|
+
children,
|
|
31
|
+
...props
|
|
32
|
+
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
|
|
33
|
+
return (
|
|
34
|
+
<TooltipPrimitive.Portal>
|
|
35
|
+
<TooltipPrimitive.Content
|
|
36
|
+
data-slot="tooltip-content"
|
|
37
|
+
sideOffset={sideOffset}
|
|
38
|
+
className={cn(
|
|
39
|
+
"bg-foreground text-background animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance",
|
|
40
|
+
className,
|
|
41
|
+
)}
|
|
42
|
+
{...props}
|
|
43
|
+
>
|
|
44
|
+
{children}
|
|
45
|
+
<TooltipPrimitive.Arrow className="bg-foreground fill-foreground z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
|
|
46
|
+
</TooltipPrimitive.Content>
|
|
47
|
+
</TooltipPrimitive.Portal>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
|
package/src/frontend.tsx
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file is the entry point for the React app, it sets up the root
|
|
3
|
+
* element and renders the App component to the DOM.
|
|
4
|
+
*
|
|
5
|
+
* It is included in `src/index.html`.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { StrictMode } from "react";
|
|
9
|
+
import { createRoot } from "react-dom/client";
|
|
10
|
+
import { App } from "./App";
|
|
11
|
+
|
|
12
|
+
const elem = document.getElementById("root")!;
|
|
13
|
+
const app = (
|
|
14
|
+
<StrictMode>
|
|
15
|
+
<App />
|
|
16
|
+
</StrictMode>
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
if (import.meta.hot) {
|
|
20
|
+
// With hot module reloading, `import.meta.hot.data` is persisted.
|
|
21
|
+
const root = (import.meta.hot.data.root ??= createRoot(elem));
|
|
22
|
+
root.render(app);
|
|
23
|
+
} else {
|
|
24
|
+
// The hot module reloading API is not available in production.
|
|
25
|
+
createRoot(elem).render(app);
|
|
26
|
+
}
|
package/src/index.css
ADDED
package/src/index.html
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Bun + React</title>
|
|
7
|
+
<script type="module" src="./frontend.tsx" async></script>
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<div id="root"></div>
|
|
11
|
+
</body>
|
|
12
|
+
</html>
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { serve } from "bun";
|
|
3
|
+
import index from "./index.html";
|
|
4
|
+
import { handleProxy } from "./proxy/handler";
|
|
5
|
+
import { getFilteredLogs, getModels, getSessions } from "./proxy/store";
|
|
6
|
+
|
|
7
|
+
const server = serve({
|
|
8
|
+
port: 25947,
|
|
9
|
+
routes: {
|
|
10
|
+
"/api/hello": {
|
|
11
|
+
GET(_req) {
|
|
12
|
+
return Response.json({
|
|
13
|
+
message: "Hello, world!",
|
|
14
|
+
method: "GET",
|
|
15
|
+
});
|
|
16
|
+
},
|
|
17
|
+
PUT(_req) {
|
|
18
|
+
return Response.json({
|
|
19
|
+
message: "Hello, world!",
|
|
20
|
+
method: "PUT",
|
|
21
|
+
});
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
"/api/hello/:name": (req) => {
|
|
26
|
+
const name = req.params.name;
|
|
27
|
+
return Response.json({
|
|
28
|
+
message: `Hello, ${name}!`,
|
|
29
|
+
});
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
"/api/logs": (req) => {
|
|
33
|
+
const url = new URL(req.url);
|
|
34
|
+
const sessionId = url.searchParams.get("sessionId") ?? undefined;
|
|
35
|
+
const model = url.searchParams.get("model") ?? undefined;
|
|
36
|
+
return Response.json(getFilteredLogs(sessionId, model));
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
"/api/sessions": () => Response.json(getSessions()),
|
|
40
|
+
|
|
41
|
+
"/api/models": () => Response.json(getModels()),
|
|
42
|
+
|
|
43
|
+
"/proxy/*": (req) => handleProxy(req),
|
|
44
|
+
|
|
45
|
+
"/*": index,
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
development: process.env.NODE_ENV !== "production" && {
|
|
49
|
+
hmr: true,
|
|
50
|
+
console: true,
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
console.log(`🚀 Server running at ${server.url}`);
|
|
55
|
+
console.log(` Proxy: ${server.url}proxy`);
|
|
56
|
+
console.log(` Configure: ANTHROPIC_BASE_URL=${server.url}proxy claude`);
|
package/src/lib/utils.ts
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { createLog, type CapturedLog } from "./store";
|
|
2
|
+
import { ClaudeResponseSchema, SseEventSchema } from "./schemas";
|
|
3
|
+
|
|
4
|
+
const UPSTREAM = "https://api.anthropic.com";
|
|
5
|
+
|
|
6
|
+
const STRIP_REQUEST_HEADERS = new Set([
|
|
7
|
+
"host",
|
|
8
|
+
"connection",
|
|
9
|
+
"keep-alive",
|
|
10
|
+
"transfer-encoding",
|
|
11
|
+
"te",
|
|
12
|
+
"upgrade",
|
|
13
|
+
"proxy-authorization",
|
|
14
|
+
"proxy-connection",
|
|
15
|
+
]);
|
|
16
|
+
|
|
17
|
+
function extractStreamContent(raw: string, log: CapturedLog): string {
|
|
18
|
+
const textParts: string[] = [];
|
|
19
|
+
|
|
20
|
+
for (const line of raw.split("\n")) {
|
|
21
|
+
if (!line.startsWith("data: ")) continue;
|
|
22
|
+
try {
|
|
23
|
+
const json: unknown = JSON.parse(line.slice(6));
|
|
24
|
+
const parsed = SseEventSchema.safeParse(json);
|
|
25
|
+
if (!parsed.success) continue;
|
|
26
|
+
const data = parsed.data;
|
|
27
|
+
if (data.type === "message_start") {
|
|
28
|
+
log.inputTokens = data.message.usage.input_tokens;
|
|
29
|
+
if (log.model === null) log.model = data.message.model;
|
|
30
|
+
}
|
|
31
|
+
if (data.type === "content_block_delta" && data.delta.type === "text_delta") {
|
|
32
|
+
textParts.push(data.delta.text);
|
|
33
|
+
}
|
|
34
|
+
if (data.type === "message_delta") {
|
|
35
|
+
log.outputTokens = data.usage.output_tokens;
|
|
36
|
+
}
|
|
37
|
+
} catch {
|
|
38
|
+
// non-JSON SSE line, skip
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return textParts.join("");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function handleProxy(req: Request): Promise<Response> {
|
|
46
|
+
const url = new URL(req.url);
|
|
47
|
+
const apiPath = url.pathname.replace(/^\/proxy/, "") + url.search;
|
|
48
|
+
const upstreamUrl = UPSTREAM + apiPath;
|
|
49
|
+
const startTime = Date.now();
|
|
50
|
+
|
|
51
|
+
const upstreamHeaders = new Headers();
|
|
52
|
+
req.headers.forEach((v, k) => {
|
|
53
|
+
if (!STRIP_REQUEST_HEADERS.has(k)) {
|
|
54
|
+
upstreamHeaders.set(k, v);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
upstreamHeaders.set("host", "api.anthropic.com");
|
|
58
|
+
upstreamHeaders.delete("accept-encoding");
|
|
59
|
+
|
|
60
|
+
let requestBody: string | null = null;
|
|
61
|
+
if (req.body && req.method !== "GET" && req.method !== "HEAD") {
|
|
62
|
+
requestBody = await req.text();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const messagesPath = apiPath.split("?")[0];
|
|
66
|
+
const isMessages = req.method === "POST" && messagesPath === "/v1/messages";
|
|
67
|
+
const log = isMessages ? createLog(req.method, apiPath, requestBody) : null;
|
|
68
|
+
|
|
69
|
+
let upstreamRes: Response;
|
|
70
|
+
try {
|
|
71
|
+
upstreamRes = await fetch(upstreamUrl, {
|
|
72
|
+
method: req.method,
|
|
73
|
+
headers: upstreamHeaders,
|
|
74
|
+
body: requestBody,
|
|
75
|
+
});
|
|
76
|
+
} catch (err) {
|
|
77
|
+
if (log) {
|
|
78
|
+
log.elapsedMs = Date.now() - startTime;
|
|
79
|
+
log.responseStatus = 502;
|
|
80
|
+
log.responseText = String(err);
|
|
81
|
+
}
|
|
82
|
+
return new Response(`Proxy error: ${err}`, { status: 502 });
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const responseHeaders = new Headers(upstreamRes.headers);
|
|
86
|
+
responseHeaders.delete("content-encoding");
|
|
87
|
+
responseHeaders.delete("content-length");
|
|
88
|
+
|
|
89
|
+
const isStream = upstreamRes.headers.get("content-type")?.includes("text/event-stream") ?? false;
|
|
90
|
+
|
|
91
|
+
if (!isStream) {
|
|
92
|
+
const responseBody = await upstreamRes.text();
|
|
93
|
+
if (log) {
|
|
94
|
+
log.elapsedMs = Date.now() - startTime;
|
|
95
|
+
log.responseStatus = upstreamRes.status;
|
|
96
|
+
try {
|
|
97
|
+
const json: unknown = JSON.parse(responseBody);
|
|
98
|
+
log.responseText = JSON.stringify(json);
|
|
99
|
+
const parsed = ClaudeResponseSchema.safeParse(json);
|
|
100
|
+
if (parsed.success) {
|
|
101
|
+
log.inputTokens = parsed.data.usage.input_tokens;
|
|
102
|
+
log.outputTokens = parsed.data.usage.output_tokens;
|
|
103
|
+
}
|
|
104
|
+
} catch {
|
|
105
|
+
log.responseText = responseBody;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return new Response(responseBody, {
|
|
109
|
+
status: upstreamRes.status,
|
|
110
|
+
headers: responseHeaders,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (log) {
|
|
115
|
+
log.streaming = true;
|
|
116
|
+
log.responseStatus = upstreamRes.status;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const chunks: string[] = [];
|
|
120
|
+
const decoder = new TextDecoder();
|
|
121
|
+
|
|
122
|
+
const transform = new TransformStream<Uint8Array, Uint8Array>({
|
|
123
|
+
transform(chunk, controller) {
|
|
124
|
+
controller.enqueue(chunk);
|
|
125
|
+
chunks.push(decoder.decode(chunk, { stream: true }));
|
|
126
|
+
},
|
|
127
|
+
flush() {
|
|
128
|
+
if (log) {
|
|
129
|
+
const full = chunks.join("");
|
|
130
|
+
log.elapsedMs = Date.now() - startTime;
|
|
131
|
+
log.responseText = extractStreamContent(full, log);
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
if (upstreamRes.body === null) {
|
|
137
|
+
return new Response("No response body", { status: 502 });
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const loggedStream = upstreamRes.body.pipeThrough(transform);
|
|
141
|
+
|
|
142
|
+
return new Response(loggedStream, {
|
|
143
|
+
status: upstreamRes.status,
|
|
144
|
+
headers: responseHeaders,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue };
|
|
4
|
+
|
|
5
|
+
const JsonValueSchema: z.ZodType<JsonValue> = z.lazy(() =>
|
|
6
|
+
z.union([
|
|
7
|
+
z.string(),
|
|
8
|
+
z.number(),
|
|
9
|
+
z.boolean(),
|
|
10
|
+
z.null(),
|
|
11
|
+
z.array(JsonValueSchema),
|
|
12
|
+
z.record(z.string(), JsonValueSchema),
|
|
13
|
+
]),
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
const CacheControl = z.object({
|
|
17
|
+
type: z.string(),
|
|
18
|
+
ttl: z.string(),
|
|
19
|
+
scope: z.string().optional(),
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const TextContentBlock = z.object({
|
|
23
|
+
type: z.literal("text"),
|
|
24
|
+
text: z.string(),
|
|
25
|
+
cache_control: CacheControl.optional(),
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const ThinkingContentBlock = z.object({
|
|
29
|
+
type: z.literal("thinking"),
|
|
30
|
+
thinking: z.string(),
|
|
31
|
+
signature: z.string().optional(),
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const ImageSourceBlock = z.object({
|
|
35
|
+
type: z.literal("base64"),
|
|
36
|
+
media_type: z.string(),
|
|
37
|
+
data: z.string(),
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const ImageContentBlock = z.object({
|
|
41
|
+
type: z.literal("image"),
|
|
42
|
+
source: ImageSourceBlock,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const ToolUseContentBlock = z.object({
|
|
46
|
+
type: z.literal("tool_use"),
|
|
47
|
+
id: z.string(),
|
|
48
|
+
name: z.string(),
|
|
49
|
+
input: z.record(z.string(), JsonValueSchema),
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const ToolResultContentItem = z.discriminatedUnion("type", [TextContentBlock, ImageContentBlock]);
|
|
53
|
+
|
|
54
|
+
const ToolResultContentBlock = z.object({
|
|
55
|
+
type: z.literal("tool_result"),
|
|
56
|
+
tool_use_id: z.string().optional(),
|
|
57
|
+
content: z.union([z.string(), z.array(ToolResultContentItem)]),
|
|
58
|
+
is_error: z.boolean().optional(),
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const ContentBlock = z.discriminatedUnion("type", [
|
|
62
|
+
TextContentBlock,
|
|
63
|
+
ThinkingContentBlock,
|
|
64
|
+
ImageContentBlock,
|
|
65
|
+
ToolUseContentBlock,
|
|
66
|
+
ToolResultContentBlock,
|
|
67
|
+
]);
|
|
68
|
+
|
|
69
|
+
const MessageContent = z.union([z.string(), z.array(ContentBlock)]);
|
|
70
|
+
|
|
71
|
+
const Message = z.object({
|
|
72
|
+
role: z.enum(["user", "assistant"]),
|
|
73
|
+
content: MessageContent,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const SystemBlock = z.object({
|
|
77
|
+
type: z.literal("text"),
|
|
78
|
+
text: z.string(),
|
|
79
|
+
cache_control: CacheControl.optional(),
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const InputSchema = z.object({
|
|
83
|
+
type: z.string(),
|
|
84
|
+
properties: z.record(z.string(), z.record(z.string(), JsonValueSchema)),
|
|
85
|
+
required: z.array(z.string()).optional(),
|
|
86
|
+
additionalProperties: z.boolean().optional(),
|
|
87
|
+
$schema: z.string().optional(),
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const ToolDefinition = z.object({
|
|
91
|
+
name: z.string(),
|
|
92
|
+
description: z.string().optional(),
|
|
93
|
+
input_schema: InputSchema.optional(),
|
|
94
|
+
cache_control: CacheControl.optional(),
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const ThinkingConfig = z.discriminatedUnion("type", [
|
|
98
|
+
z.object({ type: z.literal("enabled"), budget_tokens: z.number() }),
|
|
99
|
+
z.object({ type: z.literal("disabled") }),
|
|
100
|
+
z.object({ type: z.literal("adaptive") }),
|
|
101
|
+
]);
|
|
102
|
+
|
|
103
|
+
export const ClaudeRequestSchema = z.object({
|
|
104
|
+
model: z.string(),
|
|
105
|
+
messages: z.array(Message),
|
|
106
|
+
system: z.array(SystemBlock).optional(),
|
|
107
|
+
tools: z.array(ToolDefinition).optional(),
|
|
108
|
+
max_tokens: z.number().optional(),
|
|
109
|
+
temperature: z.number().optional(),
|
|
110
|
+
stream: z.boolean().optional(),
|
|
111
|
+
thinking: ThinkingConfig.optional(),
|
|
112
|
+
metadata: z
|
|
113
|
+
.object({
|
|
114
|
+
user_id: z.string().optional(),
|
|
115
|
+
})
|
|
116
|
+
.optional(),
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const ResponseContentBlock = z.discriminatedUnion("type", [
|
|
120
|
+
TextContentBlock,
|
|
121
|
+
ThinkingContentBlock,
|
|
122
|
+
ToolUseContentBlock,
|
|
123
|
+
]);
|
|
124
|
+
|
|
125
|
+
const ResponseUsageSchema = z.object({
|
|
126
|
+
input_tokens: z.number(),
|
|
127
|
+
output_tokens: z.number(),
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
export const ClaudeResponseSchema = z.object({
|
|
131
|
+
id: z.string(),
|
|
132
|
+
type: z.literal("message"),
|
|
133
|
+
model: z.string(),
|
|
134
|
+
role: z.literal("assistant"),
|
|
135
|
+
content: z.array(ResponseContentBlock),
|
|
136
|
+
stop_reason: z.string().nullable(),
|
|
137
|
+
stop_sequence: z.string().nullable(),
|
|
138
|
+
usage: ResponseUsageSchema,
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
const SseMessageStartEvent = z.object({
|
|
142
|
+
type: z.literal("message_start"),
|
|
143
|
+
message: z.object({
|
|
144
|
+
id: z.string(),
|
|
145
|
+
type: z.literal("message"),
|
|
146
|
+
model: z.string(),
|
|
147
|
+
role: z.literal("assistant"),
|
|
148
|
+
content: z.array(ResponseContentBlock),
|
|
149
|
+
stop_reason: z.null(),
|
|
150
|
+
stop_sequence: z.null(),
|
|
151
|
+
usage: z.object({ input_tokens: z.number() }),
|
|
152
|
+
}),
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
const SseContentBlockStartEvent = z.object({
|
|
156
|
+
type: z.literal("content_block_start"),
|
|
157
|
+
index: z.number(),
|
|
158
|
+
content_block: ResponseContentBlock,
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
const SseDeltaBlock = z.discriminatedUnion("type", [
|
|
162
|
+
z.object({ type: z.literal("text_delta"), text: z.string() }),
|
|
163
|
+
z.object({ type: z.literal("input_json_delta"), partial_json: z.string() }),
|
|
164
|
+
z.object({ type: z.literal("thinking_delta"), thinking: z.string() }),
|
|
165
|
+
z.object({ type: z.literal("signature_delta"), signature: z.string() }),
|
|
166
|
+
]);
|
|
167
|
+
|
|
168
|
+
const SseContentBlockDeltaEvent = z.object({
|
|
169
|
+
type: z.literal("content_block_delta"),
|
|
170
|
+
index: z.number(),
|
|
171
|
+
delta: SseDeltaBlock,
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
const SseContentBlockStopEvent = z.object({
|
|
175
|
+
type: z.literal("content_block_stop"),
|
|
176
|
+
index: z.number(),
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
const SseMessageDeltaEvent = z.object({
|
|
180
|
+
type: z.literal("message_delta"),
|
|
181
|
+
delta: z.object({
|
|
182
|
+
stop_reason: z.string().nullable(),
|
|
183
|
+
stop_sequence: z.string().nullable(),
|
|
184
|
+
}),
|
|
185
|
+
usage: z.object({ output_tokens: z.number() }),
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
const SseMessageStopEvent = z.object({
|
|
189
|
+
type: z.literal("message_stop"),
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
const SsePingEvent = z.object({
|
|
193
|
+
type: z.literal("ping"),
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
export const SseEventSchema = z.discriminatedUnion("type", [
|
|
197
|
+
SseMessageStartEvent,
|
|
198
|
+
SseContentBlockStartEvent,
|
|
199
|
+
SseContentBlockDeltaEvent,
|
|
200
|
+
SseContentBlockStopEvent,
|
|
201
|
+
SseMessageDeltaEvent,
|
|
202
|
+
SseMessageStopEvent,
|
|
203
|
+
SsePingEvent,
|
|
204
|
+
]);
|
|
205
|
+
|
|
206
|
+
export const CapturedLogSchema = z.object({
|
|
207
|
+
id: z.number(),
|
|
208
|
+
timestamp: z.string(),
|
|
209
|
+
method: z.string(),
|
|
210
|
+
path: z.string(),
|
|
211
|
+
model: z.string().nullable(),
|
|
212
|
+
sessionId: z.string().nullable(),
|
|
213
|
+
rawRequestBody: z.string().nullable(),
|
|
214
|
+
responseStatus: z.number().nullable(),
|
|
215
|
+
responseText: z.string().nullable(),
|
|
216
|
+
inputTokens: z.number().nullable(),
|
|
217
|
+
outputTokens: z.number().nullable(),
|
|
218
|
+
elapsedMs: z.number().nullable(),
|
|
219
|
+
streaming: z.boolean(),
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
export function parseRequest(rawBody: string | null): ClaudeRequest | null {
|
|
223
|
+
if (rawBody === null) return null;
|
|
224
|
+
try {
|
|
225
|
+
const json: unknown = JSON.parse(rawBody);
|
|
226
|
+
const result = ClaudeRequestSchema.safeParse(json);
|
|
227
|
+
if (result.success) return result.data;
|
|
228
|
+
return null;
|
|
229
|
+
} catch {
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export type ClaudeRequest = z.infer<typeof ClaudeRequestSchema>;
|
|
235
|
+
export type ClaudeResponse = z.infer<typeof ClaudeResponseSchema>;
|
|
236
|
+
export type CapturedLog = z.infer<typeof CapturedLogSchema>;
|
|
237
|
+
export type ContentBlockType = z.infer<typeof ContentBlock>;
|
|
238
|
+
export type ResponseContentBlockType = z.infer<typeof ResponseContentBlock>;
|
|
239
|
+
export type MessageType = z.infer<typeof Message>;
|
|
240
|
+
export type SystemBlockType = z.infer<typeof SystemBlock>;
|
|
241
|
+
export type ToolDefinitionType = z.infer<typeof ToolDefinition>;
|