@tonyclaw/llm-inspector 1.9.5 → 1.9.7
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 +1 -1
- package/.output/public/assets/index-Cc1oV0hF.css +1 -0
- package/.output/public/assets/index-DTjsqi6U.js +11 -0
- package/.output/public/assets/index-DrYcBTSK.js +122 -0
- package/.output/server/_chunks/ssr-renderer.mjs +1 -0
- package/.output/server/_libs/@radix-ui/react-use-controllable-state+[...].mjs +1 -1
- package/.output/server/_libs/ajv-formats.mjs +18 -18
- package/.output/server/_libs/ajv.mjs +196 -196
- package/.output/server/_libs/cookie-es.mjs +7 -21
- package/.output/server/_libs/h3-v2.mjs +18 -7
- package/.output/server/_libs/h3.mjs +24 -16
- package/.output/server/_libs/jszip.mjs +28 -28
- package/.output/server/_libs/pako.mjs +13 -13
- package/.output/server/_libs/radix-ui__react-collection.mjs +1 -1
- package/.output/server/_libs/radix-ui__react-id.mjs +1 -1
- package/.output/server/_libs/react-dom.mjs +5 -5
- package/.output/server/_libs/react.mjs +43 -43
- package/.output/server/_libs/readable-stream.mjs +15 -15
- package/.output/server/_libs/safe-buffer.mjs +3 -3
- package/.output/server/_libs/semver.mjs +10 -10
- package/.output/server/_libs/seroval-plugins.mjs +5 -5
- package/.output/server/_libs/seroval.mjs +606 -596
- package/.output/server/_libs/srvx.mjs +110 -46
- package/.output/server/_libs/swr.mjs +1 -1
- package/.output/server/_libs/tanstack__history.mjs +31 -44
- package/.output/server/_libs/tanstack__react-router.mjs +781 -1090
- package/.output/server/_libs/tanstack__router-core.mjs +2223 -2328
- package/.output/server/_libs/tslib.mjs +5 -5
- package/.output/server/_libs/use-sync-external-store.mjs +1 -1
- package/.output/server/_libs/zod.mjs +503 -205
- package/.output/server/_ssr/empty-plugin-adapters-BFgPZ6_d.mjs +6 -0
- package/.output/server/_ssr/{index-Ou5OlbF7.mjs → index-Lxfn0bBE.mjs} +53 -25
- package/.output/server/_ssr/index.mjs +1100 -777
- package/.output/server/_ssr/{router-pQnqiQaV.mjs → router-CXva8nm-.mjs} +26 -7
- package/.output/server/_tanstack-start-manifest_v-Cb2CDJtB.mjs +4 -0
- package/.output/server/index.mjs +23 -22
- package/README.md +50 -11
- package/package.json +1 -1
- package/src/components/providers/ProviderCard.tsx +26 -9
- package/src/components/proxy-viewer/formats/anthropic/ContentBlocks.tsx +35 -2
- package/src/components/proxy-viewer/formats/openai/ResponseView.tsx +25 -11
- package/src/proxy/formats/openai/schemas.ts +6 -0
- package/src/proxy/formats/openai/stream.ts +8 -0
- package/src/proxy/handler.ts +6 -2
- package/.output/public/assets/index-BrRzz6xk.js +0 -97
- package/.output/public/assets/index-DdJSLfxK.css +0 -1
- package/.output/public/assets/main-DlRlP_aH.js +0 -17
- package/.output/server/_libs/tiny-invariant.mjs +0 -12
- package/.output/server/_libs/tiny-warning.mjs +0 -5
- package/.output/server/_tanstack-start-manifest_v-DqXd4TXM.mjs +0 -4
|
@@ -9,11 +9,9 @@ import { randomUUID } from "crypto";
|
|
|
9
9
|
import { exec } from "node:child_process";
|
|
10
10
|
import { promisify } from "node:util";
|
|
11
11
|
import { o as object, s as string, _ as _enum, u as union, a as array, n as number, d as discriminatedUnion, l as literal, b as boolean, r as record, c as lazy, e as _null, f as unknown } from "../_libs/zod.mjs";
|
|
12
|
-
import "../_libs/tiny-warning.mjs";
|
|
13
12
|
import "../_libs/tanstack__router-core.mjs";
|
|
14
|
-
import "../_libs/cookie-es.mjs";
|
|
15
13
|
import "../_libs/tanstack__history.mjs";
|
|
16
|
-
import "../_libs/
|
|
14
|
+
import "../_libs/cookie-es.mjs";
|
|
17
15
|
import "../_libs/seroval.mjs";
|
|
18
16
|
import "../_libs/seroval-plugins.mjs";
|
|
19
17
|
import "node:stream/web";
|
|
@@ -44,7 +42,7 @@ import "../_libs/debounce-fn.mjs";
|
|
|
44
42
|
import "../_libs/mimic-function.mjs";
|
|
45
43
|
import "../_libs/semver.mjs";
|
|
46
44
|
import "../_libs/uint8array-extras.mjs";
|
|
47
|
-
const appCss = "/assets/index-
|
|
45
|
+
const appCss = "/assets/index-Cc1oV0hF.css";
|
|
48
46
|
const Route$g = createRootRoute({
|
|
49
47
|
head: () => ({
|
|
50
48
|
meta: [
|
|
@@ -68,7 +66,7 @@ function RootDocument({ children }) {
|
|
|
68
66
|
] })
|
|
69
67
|
] });
|
|
70
68
|
}
|
|
71
|
-
const $$splitComponentImporter = () => import("./index-
|
|
69
|
+
const $$splitComponentImporter = () => import("./index-Lxfn0bBE.mjs");
|
|
72
70
|
const Route$f = createFileRoute("/")({
|
|
73
71
|
component: lazyRouteComponent($$splitComponentImporter, "component")
|
|
74
72
|
});
|
|
@@ -527,7 +525,11 @@ const OpenAIMessage = object({
|
|
|
527
525
|
role: _enum(["system", "user", "assistant", "tool"]),
|
|
528
526
|
content: OpenAIMessageContent,
|
|
529
527
|
name: string().optional(),
|
|
530
|
-
reasoning_content: string().optional()
|
|
528
|
+
reasoning_content: string().optional(),
|
|
529
|
+
thinking: string().optional(),
|
|
530
|
+
// Some providers use 'thinking' field
|
|
531
|
+
think: string().optional()
|
|
532
|
+
// MiniMax uses 'think' field
|
|
531
533
|
});
|
|
532
534
|
const OpenAIFunctionCall = object({
|
|
533
535
|
name: string(),
|
|
@@ -563,6 +565,10 @@ const OpenAIChoiceDelta = object({
|
|
|
563
565
|
role: _enum(["assistant"]).optional(),
|
|
564
566
|
content: string().nullable().optional(),
|
|
565
567
|
reasoning_content: string().nullable().optional(),
|
|
568
|
+
thinking: string().nullable().optional(),
|
|
569
|
+
// Some providers use 'thinking' field
|
|
570
|
+
think: string().nullable().optional(),
|
|
571
|
+
// MiniMax uses 'think' field
|
|
566
572
|
function_call: object({ name: string().optional(), arguments: string().optional() }).nullable().optional(),
|
|
567
573
|
tool_calls: array(
|
|
568
574
|
object({
|
|
@@ -582,6 +588,10 @@ const OpenAIChoice = object({
|
|
|
582
588
|
role: _enum(["assistant"]),
|
|
583
589
|
content: string().nullable(),
|
|
584
590
|
reasoning_content: string().optional(),
|
|
591
|
+
thinking: string().optional(),
|
|
592
|
+
// Some providers use 'thinking' field in message
|
|
593
|
+
think: string().optional(),
|
|
594
|
+
// MiniMax uses 'think' field in message
|
|
585
595
|
function_call: object({ name: string(), arguments: string() }).nullable().optional()
|
|
586
596
|
}).optional(),
|
|
587
597
|
delta: OpenAIChoiceDelta.optional(),
|
|
@@ -1372,6 +1382,13 @@ function extractOpenAIStream(raw, log, fallbackModel, collectChunks = true) {
|
|
|
1372
1382
|
if (delta.reasoning_content !== void 0 && delta.reasoning_content !== null) {
|
|
1373
1383
|
reasoningContent += delta.reasoning_content;
|
|
1374
1384
|
}
|
|
1385
|
+
if (delta.thinking !== void 0 && delta.thinking !== null) {
|
|
1386
|
+
reasoningContent += delta.thinking;
|
|
1387
|
+
}
|
|
1388
|
+
const thinkValue = delta.think;
|
|
1389
|
+
if (thinkValue !== void 0 && thinkValue !== null) {
|
|
1390
|
+
reasoningContent += thinkValue;
|
|
1391
|
+
}
|
|
1375
1392
|
if (choice.finish_reason !== void 0 && choice.finish_reason !== null) {
|
|
1376
1393
|
finishReason = choice.finish_reason;
|
|
1377
1394
|
}
|
|
@@ -2112,7 +2129,9 @@ async function handleProxy(req) {
|
|
|
2112
2129
|
return new Response("Forbidden: unsupported provider", { status: STATUS_FORBIDDEN });
|
|
2113
2130
|
}
|
|
2114
2131
|
let formatHandler;
|
|
2115
|
-
if (
|
|
2132
|
+
if (parsed.isChatCompletions) {
|
|
2133
|
+
formatHandler = formatRegistry.get("openai") ?? null;
|
|
2134
|
+
} else if (matchedProviderConfig?.format) {
|
|
2116
2135
|
formatHandler = formatRegistry.get(matchedProviderConfig.format) ?? null;
|
|
2117
2136
|
} else {
|
|
2118
2137
|
formatHandler = formatForPath(parsed.apiPath);
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
const tsrStartManifest = () => ({ routes: { __root__: { filePath: "C:/Users/claw/workspace/llm-inspector/src/routes/__root.tsx", children: ["/", "/api/health", "/api/logs", "/api/models", "/api/providers", "/api/sessions", "/proxy/$", "/api/config/paths"], preloads: ["/assets/index-DTjsqi6U.js"], scripts: [{ attrs: { type: "module", async: true, src: "/assets/index-DTjsqi6U.js" } }] }, "/": { filePath: "C:/Users/claw/workspace/llm-inspector/src/routes/index.tsx", children: void 0, preloads: ["/assets/index-DrYcBTSK.js"] } } });
|
|
2
|
+
export {
|
|
3
|
+
tsrStartManifest
|
|
4
|
+
};
|
package/.output/server/index.mjs
CHANGED
|
@@ -7,6 +7,7 @@ import { fileURLToPath } from "node:url";
|
|
|
7
7
|
import { dirname, resolve } from "node:path";
|
|
8
8
|
import "node:http";
|
|
9
9
|
import "node:stream";
|
|
10
|
+
import "node:stream/promises";
|
|
10
11
|
import "node:https";
|
|
11
12
|
import "node:http2";
|
|
12
13
|
import "./_libs/rou3.mjs";
|
|
@@ -100,51 +101,51 @@ const assets = {
|
|
|
100
101
|
"/assets/alibaba-TTwafVwX.svg": {
|
|
101
102
|
"type": "image/svg+xml",
|
|
102
103
|
"etag": '"171b-6dyV5K8QjiaY35sN9qNprh9zDIs"',
|
|
103
|
-
"mtime": "2026-06-
|
|
104
|
+
"mtime": "2026-06-05T11:18:11.305Z",
|
|
104
105
|
"size": 5915,
|
|
105
106
|
"path": "../public/assets/alibaba-TTwafVwX.svg"
|
|
106
107
|
},
|
|
107
|
-
"/assets/index-DdJSLfxK.css": {
|
|
108
|
-
"type": "text/css; charset=utf-8",
|
|
109
|
-
"etag": '"10da0-LYeZ5d/vwqh4bAnuP/9hr6Wka6g"',
|
|
110
|
-
"mtime": "2026-06-04T07:50:34.983Z",
|
|
111
|
-
"size": 69024,
|
|
112
|
-
"path": "../public/assets/index-DdJSLfxK.css"
|
|
113
|
-
},
|
|
114
108
|
"/assets/zhipuai-BPNAnxo-.svg": {
|
|
115
109
|
"type": "image/svg+xml",
|
|
116
110
|
"etag": '"2bf8-hNaLCTi89nOFCsIIfWpP/jrfo0s"',
|
|
117
|
-
"mtime": "2026-06-
|
|
111
|
+
"mtime": "2026-06-05T11:18:11.305Z",
|
|
118
112
|
"size": 11256,
|
|
119
113
|
"path": "../public/assets/zhipuai-BPNAnxo-.svg"
|
|
120
114
|
},
|
|
121
|
-
"/assets/
|
|
122
|
-
"type": "text/
|
|
123
|
-
"etag": '"
|
|
124
|
-
"mtime": "2026-06-
|
|
125
|
-
"size":
|
|
126
|
-
"path": "../public/assets/
|
|
115
|
+
"/assets/index-Cc1oV0hF.css": {
|
|
116
|
+
"type": "text/css; charset=utf-8",
|
|
117
|
+
"etag": '"10d3b-Y2lH7o+AtG0LDeZFMgPtKHzDOAQ"',
|
|
118
|
+
"mtime": "2026-06-05T11:18:11.307Z",
|
|
119
|
+
"size": 68923,
|
|
120
|
+
"path": "../public/assets/index-Cc1oV0hF.css"
|
|
127
121
|
},
|
|
128
122
|
"/assets/minimax-BPMzvuL-.jpeg": {
|
|
129
123
|
"type": "image/jpeg",
|
|
130
124
|
"etag": '"1b06-IwivU89ko5UTMUM1/t7hn4sQK9A"',
|
|
131
|
-
"mtime": "2026-06-
|
|
125
|
+
"mtime": "2026-06-05T11:18:11.307Z",
|
|
132
126
|
"size": 6918,
|
|
133
127
|
"path": "../public/assets/minimax-BPMzvuL-.jpeg"
|
|
134
128
|
},
|
|
129
|
+
"/assets/index-DTjsqi6U.js": {
|
|
130
|
+
"type": "text/javascript; charset=utf-8",
|
|
131
|
+
"etag": '"51034-6xxprFE2mCIi4bRjy5msIBNoJig"',
|
|
132
|
+
"mtime": "2026-06-05T11:18:11.307Z",
|
|
133
|
+
"size": 331828,
|
|
134
|
+
"path": "../public/assets/index-DTjsqi6U.js"
|
|
135
|
+
},
|
|
135
136
|
"/assets/qwen-CONDcHqt.png": {
|
|
136
137
|
"type": "image/png",
|
|
137
138
|
"etag": '"572c3-cdJAPaHdOvFCGzuaQjagdgOu6XE"',
|
|
138
|
-
"mtime": "2026-06-
|
|
139
|
+
"mtime": "2026-06-05T11:18:11.307Z",
|
|
139
140
|
"size": 357059,
|
|
140
141
|
"path": "../public/assets/qwen-CONDcHqt.png"
|
|
141
142
|
},
|
|
142
|
-
"/assets/index-
|
|
143
|
+
"/assets/index-DrYcBTSK.js": {
|
|
143
144
|
"type": "text/javascript; charset=utf-8",
|
|
144
|
-
"etag": '"
|
|
145
|
-
"mtime": "2026-06-
|
|
146
|
-
"size":
|
|
147
|
-
"path": "../public/assets/index-
|
|
145
|
+
"etag": '"85db6-JHWqbScXD08PJnzmagxJRT95Oek"',
|
|
146
|
+
"mtime": "2026-06-05T11:18:11.307Z",
|
|
147
|
+
"size": 548278,
|
|
148
|
+
"path": "../public/assets/index-DrYcBTSK.js"
|
|
148
149
|
}
|
|
149
150
|
};
|
|
150
151
|
function readAsset(id) {
|
package/README.md
CHANGED
|
@@ -4,23 +4,31 @@
|
|
|
4
4
|
|
|
5
5
|

|
|
6
6
|
|
|
7
|
-
llm-inspector
|
|
7
|
+
## What is llm-inspector?
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
**llm-inspector** 是一个 LLM API 透明代理调试工具,能够捕获 AI 编程工具(Claude Code、OpenCode、Cursor、Cody 等)与 LLM 提供商之间的所有 API 请求和响应,在 Web UI 中实时展示:
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
- 系统提示词 (System Prompts)
|
|
12
|
+
- 工具定义 (Tool Definitions)
|
|
13
|
+
- 消息内容 (Messages)
|
|
14
|
+
- SSE 流式数据块 (Streaming Chunks)
|
|
15
|
+
- Token 用量 (Token Usage)
|
|
12
16
|
|
|
13
|
-
|
|
17
|
+
**专为理解 AI 编程工具底层 API 调用行为而设计。帮助开发者看清 LLM API 的"最后一公里"。**
|
|
14
18
|
|
|
15
|
-
|
|
16
|
-
- [安装 (Installation)](docs/Installation.md) - 安装指南
|
|
17
|
-
- [使用 (Usage)](docs/Usage.md) - 详细使用说明
|
|
18
|
-
- [架构 (Architecture)](docs/Architecture.md) - 系统架构
|
|
19
|
-
- [开发 (Development)](docs/Development.md) - 开发指南
|
|
19
|
+
## Key Advantages / 核心优势
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
| 优势 | 说明 |
|
|
22
|
+
|------|------|
|
|
23
|
+
| **透明代理、零侵入** | 只需设置环境变量即可拦截所有流量,无需修改 AI 编程工具本身 |
|
|
24
|
+
| **结构化 API 可视化** | 交互式 JSON 树查看器,自动解析并渲染 Markdown 文本、思考块、工具调用块 |
|
|
25
|
+
| **完整的 SSE 流式解析** | 捕获并重建每个流式事件,原始事件时间线一目了然 |
|
|
26
|
+
| **Token 用量实时追踪** | 单请求 + 会话级 + 全局级 Token 统计 |
|
|
27
|
+
| **多提供商自动路由** | 支持 Anthropic、OpenAI、DeepSeek、MiniMax、Qwen、ZhipuAI,自动识别格式和认证 |
|
|
28
|
+
| **客户端进程追踪** | 自动关联 PID、工作目录、项目文件夹,区分请求来源 |
|
|
29
|
+
| **持久化存储 + 导出** | 磁盘日志存储 + ZIP 导出,支持离线分析 |
|
|
22
30
|
|
|
23
|
-
## 快速开始
|
|
31
|
+
## Quick Start / 快速开始
|
|
24
32
|
|
|
25
33
|
### npm 安装
|
|
26
34
|
|
|
@@ -44,4 +52,35 @@ bun run dev
|
|
|
44
52
|
|
|
45
53
|
打开浏览器访问 http://localhost:25947 查看实时捕获的请求。
|
|
46
54
|
|
|
55
|
+
### 配置 AI 编程工具
|
|
56
|
+
|
|
57
|
+
配置你的 AI 编程工具,将 API 请求路由到 llm-inspector 代理:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
# Claude Code
|
|
61
|
+
ANTHROPIC_BASE_URL=http://localhost:25947/proxy claude
|
|
62
|
+
|
|
63
|
+
# OpenCode
|
|
64
|
+
LLM_BASE_URL=http://localhost:25947/proxy opencode
|
|
65
|
+
|
|
66
|
+
# Cursor, Cody 等
|
|
67
|
+
ANTHROPIC_BASE_URL=http://localhost:25947/proxy <your-tool>
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### 验证代理工作正常
|
|
71
|
+
|
|
72
|
+
1. 启动 llm-inspector 后,打开浏览器访问 http://localhost:25947
|
|
73
|
+
2. 配置好环境变量后,向代理发送一个请求
|
|
74
|
+
3. 观察 Web UI 中是否实时显示捕获的请求和响应
|
|
75
|
+
|
|
47
76
|

|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## Documentation / 文档目录
|
|
81
|
+
|
|
82
|
+
- [特性 (Features)](docs/Features.md) - 核心功能介绍
|
|
83
|
+
- [安装 (Installation)](docs/Installation.md) - 安装指南
|
|
84
|
+
- [使用 (Usage)](docs/Usage.md) - 详细使用说明
|
|
85
|
+
- [架构 (Architecture)](docs/Architecture.md) - 系统架构
|
|
86
|
+
- [开发 (Development)](docs/Development.md) - 开发指南
|
package/package.json
CHANGED
|
@@ -112,6 +112,8 @@ function getErrorIcon(type: ErrorType): JSX.Element {
|
|
|
112
112
|
}
|
|
113
113
|
|
|
114
114
|
function TestStatus({ result }: { result: TestResult | NotConfigured | Testing }): JSX.Element {
|
|
115
|
+
const [showDetails, setShowDetails] = useState(false);
|
|
116
|
+
|
|
115
117
|
if (!hasSuccessField(result)) {
|
|
116
118
|
// Not TestResult - check if NotConfigured or Testing
|
|
117
119
|
if (isNotConfiguredState(result)) {
|
|
@@ -142,18 +144,33 @@ function TestStatus({ result }: { result: TestResult | NotConfigured | Testing }
|
|
|
142
144
|
const error = result.error;
|
|
143
145
|
const errorMessage = error?.message ?? "Connection failed";
|
|
144
146
|
const errorHint = error?.hint;
|
|
147
|
+
const errorDetails = error?.details;
|
|
145
148
|
const errorType = error?.type ?? "unknown";
|
|
146
149
|
|
|
147
|
-
// Combine message and hint in a single line for consistent layout
|
|
148
|
-
const fullMessage = errorHint !== undefined ? `${errorMessage} — ${errorHint}` : errorMessage;
|
|
149
|
-
|
|
150
150
|
return (
|
|
151
|
-
<div
|
|
152
|
-
className="flex items-center gap-1 text-xs text-red-600
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
151
|
+
<div className="flex flex-col gap-1 shrink-0">
|
|
152
|
+
<div className="flex items-center gap-1 text-xs text-red-600 max-w-[200px]">
|
|
153
|
+
{getErrorIcon(errorType)}
|
|
154
|
+
<span className="truncate">{errorMessage}</span>
|
|
155
|
+
{errorDetails !== undefined && (
|
|
156
|
+
<button
|
|
157
|
+
type="button"
|
|
158
|
+
onClick={() => setShowDetails(!showDetails)}
|
|
159
|
+
className="shrink-0 text-muted-foreground hover:text-foreground transition-colors"
|
|
160
|
+
title={showDetails ? "Hide details" : "Show details"}
|
|
161
|
+
>
|
|
162
|
+
{showDetails ? <EyeOff className="size-3" /> : <Eye className="size-3" />}
|
|
163
|
+
</button>
|
|
164
|
+
)}
|
|
165
|
+
</div>
|
|
166
|
+
{showDetails && errorDetails !== undefined && (
|
|
167
|
+
<div className="text-xs text-muted-foreground bg-muted/50 rounded p-2 max-w-[300px]">
|
|
168
|
+
{errorHint !== undefined && <div className="mb-1">{errorHint}</div>}
|
|
169
|
+
<div className="font-mono whitespace-pre-wrap break-all text-red-400/80">
|
|
170
|
+
{errorDetails}
|
|
171
|
+
</div>
|
|
172
|
+
</div>
|
|
173
|
+
)}
|
|
157
174
|
</div>
|
|
158
175
|
);
|
|
159
176
|
}
|
|
@@ -37,14 +37,47 @@ function SystemReminderBlock({ text }: { text: string }): JSX.Element {
|
|
|
37
37
|
);
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
// Regex to extract content wrapped in <think>...</thinking> tags (MiniMax format: <think>...</think>)
|
|
41
|
+
const THINKING_TAG_REGEX = /<think>([\s\S]*?)<\/think>/gi;
|
|
42
|
+
const THINKING_TAG_REGEX_SINGLE = /<think>([\s\S]*?)<\/think>/i;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Extract thinking content wrapped in <think> tags (thought balloon emoji) from text.
|
|
46
|
+
* Returns { thinking, remainingText } where thinking is the extracted content
|
|
47
|
+
* and remainingText is the original text with thinking tags removed.
|
|
48
|
+
*/
|
|
49
|
+
export function extractThinkingFromContent(text: string): {
|
|
50
|
+
thinking: string | null;
|
|
51
|
+
remainingText: string;
|
|
52
|
+
} {
|
|
53
|
+
const match = THINKING_TAG_REGEX_SINGLE.exec(text);
|
|
54
|
+
if (!match || match[1] === undefined) {
|
|
55
|
+
return { thinking: null, remainingText: text };
|
|
56
|
+
}
|
|
57
|
+
const thinking = match[1].trim();
|
|
58
|
+
const remainingText = text.replace(THINKING_TAG_REGEX, "").trim();
|
|
59
|
+
return { thinking, remainingText };
|
|
60
|
+
}
|
|
61
|
+
|
|
40
62
|
export function TextBlock({ text }: { text: string }): JSX.Element {
|
|
41
63
|
if (text.includes("<system-reminder>")) {
|
|
42
64
|
return <SystemReminderBlock text={text} />;
|
|
43
65
|
}
|
|
44
66
|
|
|
67
|
+
// Check for <think> tags wrapped in content
|
|
68
|
+
const { thinking, remainingText } = extractThinkingFromContent(text);
|
|
69
|
+
|
|
45
70
|
return (
|
|
46
|
-
<div className="
|
|
47
|
-
<
|
|
71
|
+
<div className="space-y-2">
|
|
72
|
+
{thinking !== null && <ThinkingBlock thinking={thinking} />}
|
|
73
|
+
{remainingText.length > 0 && (
|
|
74
|
+
<div className="prose prose-sm dark:prose-invert max-w-none [&_pre]:bg-muted [&_pre]:text-foreground [&_code]:text-[0.8em] [&_p]:my-1 [&_ul]:my-1 [&_ol]:my-1">
|
|
75
|
+
<ReactMarkdown>{remainingText}</ReactMarkdown>
|
|
76
|
+
</div>
|
|
77
|
+
)}
|
|
78
|
+
{thinking === null && remainingText.length === 0 && (
|
|
79
|
+
<p className="text-xs text-muted-foreground italic">Empty text block</p>
|
|
80
|
+
)}
|
|
48
81
|
</div>
|
|
49
82
|
);
|
|
50
83
|
}
|
|
@@ -5,6 +5,10 @@ import type { OpenAIResponse } from "../../../../proxy/schemas";
|
|
|
5
5
|
import { formatTokens } from "../../../../lib/utils";
|
|
6
6
|
import { Badge } from "../../../ui/badge";
|
|
7
7
|
import { Separator } from "../../../ui/separator";
|
|
8
|
+
import { extractThinkingFromContent, ThinkingBlock } from "../anthropic/ContentBlocks";
|
|
9
|
+
|
|
10
|
+
// Re-export for use in other components
|
|
11
|
+
export { extractThinkingFromContent } from "../anthropic/ContentBlocks";
|
|
8
12
|
|
|
9
13
|
export function OpenAIResponseView({ response }: { response: OpenAIResponse }): JSX.Element {
|
|
10
14
|
const choice = response.choices[0];
|
|
@@ -42,20 +46,30 @@ export function OpenAIResponseView({ response }: { response: OpenAIResponse }):
|
|
|
42
46
|
{message?.reasoning_content !== null &&
|
|
43
47
|
message?.reasoning_content !== undefined &&
|
|
44
48
|
message.reasoning_content.length > 0 && (
|
|
45
|
-
<
|
|
46
|
-
<div className="text-xs text-purple-400 font-mono mb-1">thinking</div>
|
|
47
|
-
<div className="text-sm text-purple-200 whitespace-pre-wrap">
|
|
48
|
-
{message.reasoning_content}
|
|
49
|
-
</div>
|
|
50
|
-
</div>
|
|
49
|
+
<ThinkingBlock thinking={message.reasoning_content} />
|
|
51
50
|
)}
|
|
52
51
|
{message?.content !== null &&
|
|
53
52
|
message?.content !== undefined &&
|
|
54
|
-
message.content.length > 0 &&
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
53
|
+
message.content.length > 0 &&
|
|
54
|
+
(() => {
|
|
55
|
+
// Extract thinking content from tag-wrapped content if no reasoning_content field
|
|
56
|
+
const hasReasoningField =
|
|
57
|
+
message.reasoning_content !== null &&
|
|
58
|
+
message.reasoning_content !== undefined &&
|
|
59
|
+
message.reasoning_content.length > 0;
|
|
60
|
+
const { thinking, remainingText } = extractThinkingFromContent(message.content);
|
|
61
|
+
return (
|
|
62
|
+
<div className="space-y-2">
|
|
63
|
+
{/* Show thinking from tags only if no reasoning_content field */}
|
|
64
|
+
{thinking !== null && !hasReasoningField && <ThinkingBlock thinking={thinking} />}
|
|
65
|
+
{remainingText.length > 0 && (
|
|
66
|
+
<div className="prose prose-sm dark:prose-invert max-w-none [&_pre]:bg-muted [&_pre]:text-foreground [&_code]:text-[0.8em] [&_p]:my-1 [&_ul]:my-1 [&_ol]:my-1">
|
|
67
|
+
<ReactMarkdown>{remainingText}</ReactMarkdown>
|
|
68
|
+
</div>
|
|
69
|
+
)}
|
|
70
|
+
</div>
|
|
71
|
+
);
|
|
72
|
+
})()}
|
|
59
73
|
{message?.function_call !== null && message?.function_call !== undefined && (
|
|
60
74
|
<div className="border border-blue-500/30 rounded-md p-3 bg-blue-500/5">
|
|
61
75
|
<div className="text-xs text-blue-400 font-mono mb-1">function_call</div>
|
|
@@ -21,6 +21,8 @@ export const OpenAIMessage = z.object({
|
|
|
21
21
|
content: OpenAIMessageContent,
|
|
22
22
|
name: z.string().optional(),
|
|
23
23
|
reasoning_content: z.string().optional(),
|
|
24
|
+
thinking: z.string().optional(), // Some providers use 'thinking' field
|
|
25
|
+
think: z.string().optional(), // MiniMax uses 'think' field
|
|
24
26
|
});
|
|
25
27
|
|
|
26
28
|
export const OpenAIFunctionCall = z.object({
|
|
@@ -65,6 +67,8 @@ export const OpenAIChoiceDelta = z.object({
|
|
|
65
67
|
role: z.enum(["assistant"]).optional(),
|
|
66
68
|
content: z.string().nullable().optional(),
|
|
67
69
|
reasoning_content: z.string().nullable().optional(),
|
|
70
|
+
thinking: z.string().nullable().optional(), // Some providers use 'thinking' field
|
|
71
|
+
think: z.string().nullable().optional(), // MiniMax uses 'think' field
|
|
68
72
|
function_call: z
|
|
69
73
|
.object({ name: z.string().optional(), arguments: z.string().optional() })
|
|
70
74
|
.nullable()
|
|
@@ -92,6 +96,8 @@ export const OpenAIChoice = z.object({
|
|
|
92
96
|
role: z.enum(["assistant"]),
|
|
93
97
|
content: z.string().nullable(),
|
|
94
98
|
reasoning_content: z.string().optional(),
|
|
99
|
+
thinking: z.string().optional(), // Some providers use 'thinking' field in message
|
|
100
|
+
think: z.string().optional(), // MiniMax uses 'think' field in message
|
|
95
101
|
function_call: z.object({ name: z.string(), arguments: z.string() }).nullable().optional(),
|
|
96
102
|
})
|
|
97
103
|
.optional(),
|
|
@@ -99,6 +99,14 @@ export function extractOpenAIStream(
|
|
|
99
99
|
if (delta.reasoning_content !== undefined && delta.reasoning_content !== null) {
|
|
100
100
|
reasoningContent += delta.reasoning_content;
|
|
101
101
|
}
|
|
102
|
+
if (delta.thinking !== undefined && delta.thinking !== null) {
|
|
103
|
+
reasoningContent += delta.thinking;
|
|
104
|
+
}
|
|
105
|
+
// MiniMax uses 'think' field for thinking content - check via bracket notation
|
|
106
|
+
const thinkValue = delta.think;
|
|
107
|
+
if (thinkValue !== undefined && thinkValue !== null) {
|
|
108
|
+
reasoningContent += thinkValue;
|
|
109
|
+
}
|
|
102
110
|
if (choice.finish_reason !== undefined && choice.finish_reason !== null) {
|
|
103
111
|
finishReason = choice.finish_reason;
|
|
104
112
|
}
|
package/src/proxy/handler.ts
CHANGED
|
@@ -332,9 +332,13 @@ export async function handleProxy(req: Request): Promise<Response> {
|
|
|
332
332
|
return new Response("Forbidden: unsupported provider", { status: STATUS_FORBIDDEN });
|
|
333
333
|
}
|
|
334
334
|
|
|
335
|
-
// Get the format handler based on
|
|
335
|
+
// Get the format handler based on the actual upstream URL being used.
|
|
336
|
+
// For Chat Completions, the upstream always returns OpenAI SSE format regardless
|
|
337
|
+
// of provider config (which may have format=anthropic for backward compat).
|
|
336
338
|
let formatHandler: FormatHandler | null;
|
|
337
|
-
if (
|
|
339
|
+
if (parsed.isChatCompletions) {
|
|
340
|
+
formatHandler = formatRegistry.get("openai") ?? null;
|
|
341
|
+
} else if (matchedProviderConfig?.format) {
|
|
338
342
|
formatHandler = formatRegistry.get(matchedProviderConfig.format) ?? null;
|
|
339
343
|
} else {
|
|
340
344
|
formatHandler = formatForPath(parsed.apiPath);
|