@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.
Files changed (50) hide show
  1. package/.output/nitro.json +1 -1
  2. package/.output/public/assets/index-Cc1oV0hF.css +1 -0
  3. package/.output/public/assets/index-DTjsqi6U.js +11 -0
  4. package/.output/public/assets/index-DrYcBTSK.js +122 -0
  5. package/.output/server/_chunks/ssr-renderer.mjs +1 -0
  6. package/.output/server/_libs/@radix-ui/react-use-controllable-state+[...].mjs +1 -1
  7. package/.output/server/_libs/ajv-formats.mjs +18 -18
  8. package/.output/server/_libs/ajv.mjs +196 -196
  9. package/.output/server/_libs/cookie-es.mjs +7 -21
  10. package/.output/server/_libs/h3-v2.mjs +18 -7
  11. package/.output/server/_libs/h3.mjs +24 -16
  12. package/.output/server/_libs/jszip.mjs +28 -28
  13. package/.output/server/_libs/pako.mjs +13 -13
  14. package/.output/server/_libs/radix-ui__react-collection.mjs +1 -1
  15. package/.output/server/_libs/radix-ui__react-id.mjs +1 -1
  16. package/.output/server/_libs/react-dom.mjs +5 -5
  17. package/.output/server/_libs/react.mjs +43 -43
  18. package/.output/server/_libs/readable-stream.mjs +15 -15
  19. package/.output/server/_libs/safe-buffer.mjs +3 -3
  20. package/.output/server/_libs/semver.mjs +10 -10
  21. package/.output/server/_libs/seroval-plugins.mjs +5 -5
  22. package/.output/server/_libs/seroval.mjs +606 -596
  23. package/.output/server/_libs/srvx.mjs +110 -46
  24. package/.output/server/_libs/swr.mjs +1 -1
  25. package/.output/server/_libs/tanstack__history.mjs +31 -44
  26. package/.output/server/_libs/tanstack__react-router.mjs +781 -1090
  27. package/.output/server/_libs/tanstack__router-core.mjs +2223 -2328
  28. package/.output/server/_libs/tslib.mjs +5 -5
  29. package/.output/server/_libs/use-sync-external-store.mjs +1 -1
  30. package/.output/server/_libs/zod.mjs +503 -205
  31. package/.output/server/_ssr/empty-plugin-adapters-BFgPZ6_d.mjs +6 -0
  32. package/.output/server/_ssr/{index-Ou5OlbF7.mjs → index-Lxfn0bBE.mjs} +53 -25
  33. package/.output/server/_ssr/index.mjs +1100 -777
  34. package/.output/server/_ssr/{router-pQnqiQaV.mjs → router-CXva8nm-.mjs} +26 -7
  35. package/.output/server/_tanstack-start-manifest_v-Cb2CDJtB.mjs +4 -0
  36. package/.output/server/index.mjs +23 -22
  37. package/README.md +50 -11
  38. package/package.json +1 -1
  39. package/src/components/providers/ProviderCard.tsx +26 -9
  40. package/src/components/proxy-viewer/formats/anthropic/ContentBlocks.tsx +35 -2
  41. package/src/components/proxy-viewer/formats/openai/ResponseView.tsx +25 -11
  42. package/src/proxy/formats/openai/schemas.ts +6 -0
  43. package/src/proxy/formats/openai/stream.ts +8 -0
  44. package/src/proxy/handler.ts +6 -2
  45. package/.output/public/assets/index-BrRzz6xk.js +0 -97
  46. package/.output/public/assets/index-DdJSLfxK.css +0 -1
  47. package/.output/public/assets/main-DlRlP_aH.js +0 -17
  48. package/.output/server/_libs/tiny-invariant.mjs +0 -12
  49. package/.output/server/_libs/tiny-warning.mjs +0 -5
  50. 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/tiny-invariant.mjs";
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-DdJSLfxK.css";
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-Ou5OlbF7.mjs");
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 (matchedProviderConfig?.format) {
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
+ };
@@ -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-04T07:50:34.983Z",
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-04T07:50:34.981Z",
111
+ "mtime": "2026-06-05T11:18:11.305Z",
118
112
  "size": 11256,
119
113
  "path": "../public/assets/zhipuai-BPNAnxo-.svg"
120
114
  },
121
- "/assets/main-DlRlP_aH.js": {
122
- "type": "text/javascript; charset=utf-8",
123
- "etag": '"50591-9wb2koOmoSeF78CqgkKJ6J53UhQ"',
124
- "mtime": "2026-06-04T07:50:34.983Z",
125
- "size": 329105,
126
- "path": "../public/assets/main-DlRlP_aH.js"
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-04T07:50:34.983Z",
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-04T07:50:34.983Z",
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-BrRzz6xk.js": {
143
+ "/assets/index-DrYcBTSK.js": {
143
144
  "type": "text/javascript; charset=utf-8",
144
- "etag": '"84c41-ySQowuzk06PhaGymfQ18pzt8XKo"',
145
- "mtime": "2026-06-04T07:50:34.983Z",
146
- "size": 543809,
147
- "path": "../public/assets/index-BrRzz6xk.js"
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
  ![Proxy Overview](docs/Proxy.png)
6
6
 
7
- llm-inspector 是一个 LLM API 透明代理,能够捕获 AI 编程工具(Claude Code、OpenCode、Cody、Cursor 等)与 LLM 提供商之间的每一次请求和响应,在 Web UI 中实时展示系统提示词、工具定义、消息内容、SSE 流式数据块和 Token 用量。
7
+ ## What is llm-inspector?
8
8
 
9
- **专为理解 AI 编程工具底层 API 调用而设计。**
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
- - [特性 (Features)](docs/Features.md) - 核心功能介绍
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
  ![Proxy Hello](docs/Proxy%20Hello.png)
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tonyclaw/llm-inspector",
3
- "version": "1.9.5",
3
+ "version": "1.9.7",
4
4
  "type": "module",
5
5
  "description": "LLM API proxy inspector — captures and displays requests/responses from AI coding tools in a web UI",
6
6
  "license": "MIT",
@@ -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 shrink-0 max-w-[200px]"
153
- title={error?.details ?? fullMessage}
154
- >
155
- {getErrorIcon(errorType)}
156
- <span className="truncate">{errorMessage}</span>
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="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">
47
- <ReactMarkdown>{text}</ReactMarkdown>
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
- <div className="border border-purple-500/30 rounded-md p-3 bg-purple-500/5">
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
- <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">
56
- <ReactMarkdown>{message.content}</ReactMarkdown>
57
- </div>
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
  }
@@ -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 provider format (preferred) or request path fallback
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 (matchedProviderConfig?.format) {
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);