routstrd 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/.claude/settings.local.json +7 -0
- package/README.md +191 -0
- package/bun.lock +376 -0
- package/dist/index.js +27019 -0
- package/package.json +34 -0
- package/routstr-cost-logging.md +71 -0
- package/src/TUI refactor.md +113 -0
- package/src/cli-shared.ts +204 -0
- package/src/cli.ts +650 -0
- package/src/daemon/args.ts +19 -0
- package/src/daemon/config-store.ts +36 -0
- package/src/daemon/http/index.ts +608 -0
- package/src/daemon/index.ts +151 -0
- package/src/daemon/models.ts +49 -0
- package/src/daemon/sse.ts +98 -0
- package/src/daemon/types.ts +25 -0
- package/src/daemon/wallet/index.ts +207 -0
- package/src/daemon.ts +1 -0
- package/src/index.ts +4 -0
- package/src/integrations/index.ts +67 -0
- package/src/integrations/openclaw.ts +177 -0
- package/src/integrations/opencode.ts +120 -0
- package/src/integrations/pi.ts +116 -0
- package/src/start-daemon.ts +90 -0
- package/src/tui/usage/app.ts +247 -0
- package/src/tui/usage/constants.ts +42 -0
- package/src/tui/usage/data.ts +228 -0
- package/src/tui/usage/index.ts +1 -0
- package/src/tui/usage/render.ts +539 -0
- package/src/tui/usage/state.ts +100 -0
- package/src/tui/usage/terminal.ts +39 -0
- package/src/tui/usage/types.ts +65 -0
- package/src/utils/config.ts +22 -0
- package/src/utils/logger.ts +54 -0
- package/test_box.ts +15 -0
- package/test_curl.sh +11 -0
- package/test_split_box.ts +17 -0
- package/test_split_box2.ts +23 -0
- package/tsconfig.json +20 -0
- package/v1-messages-format-report.md +223 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { UsageTrackingEntry } from "../../daemon/types.ts";
|
|
2
|
+
|
|
3
|
+
export interface UsageStats {
|
|
4
|
+
entries: UsageTrackingEntry[];
|
|
5
|
+
totalEntries: number;
|
|
6
|
+
totalSatsCost: number;
|
|
7
|
+
recentSatsCost: number;
|
|
8
|
+
limit: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface DayStats {
|
|
12
|
+
date: string;
|
|
13
|
+
requests: number;
|
|
14
|
+
satsCost: number;
|
|
15
|
+
promptTokens: number;
|
|
16
|
+
completionTokens: number;
|
|
17
|
+
totalTokens: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface ModelStats {
|
|
21
|
+
modelId: string;
|
|
22
|
+
requests: number;
|
|
23
|
+
satsCost: number;
|
|
24
|
+
promptTokens: number;
|
|
25
|
+
completionTokens: number;
|
|
26
|
+
totalTokens: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface ProviderStats {
|
|
30
|
+
baseUrl: string;
|
|
31
|
+
requests: number;
|
|
32
|
+
satsCost: number;
|
|
33
|
+
promptTokens: number;
|
|
34
|
+
completionTokens: number;
|
|
35
|
+
totalTokens: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface ClientStats {
|
|
39
|
+
client: string;
|
|
40
|
+
requests: number;
|
|
41
|
+
satsCost: number;
|
|
42
|
+
promptTokens: number;
|
|
43
|
+
completionTokens: number;
|
|
44
|
+
totalTokens: number;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export type TabId = "overview" | "today" | "models" | "providers" | "tokens" | "clients" | "recent";
|
|
48
|
+
|
|
49
|
+
export interface Tab {
|
|
50
|
+
id: TabId;
|
|
51
|
+
name: string;
|
|
52
|
+
key: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface VimState {
|
|
56
|
+
scrollPos: number;
|
|
57
|
+
searchQuery: string;
|
|
58
|
+
searchResults: number[];
|
|
59
|
+
currentSearchIdx: number;
|
|
60
|
+
isSearching: boolean;
|
|
61
|
+
searchReverse: boolean;
|
|
62
|
+
mode: "normal" | "search";
|
|
63
|
+
lastKey: string;
|
|
64
|
+
lastKeyTime: number;
|
|
65
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
const HOME = process.env.HOME || process.env.USERPROFILE || "";
|
|
2
|
+
|
|
3
|
+
export const CONFIG_DIR = process.env.ROUTSTRD_DIR || `${HOME}/.routstrd`;
|
|
4
|
+
export const SOCKET_PATH = process.env.ROUTSTRD_SOCKET || `${CONFIG_DIR}/routstrd.sock`;
|
|
5
|
+
export const PID_FILE = process.env.ROUTSTRD_PID || `${CONFIG_DIR}/routstrd.pid`;
|
|
6
|
+
export const DB_PATH = `${CONFIG_DIR}/routstr.db`;
|
|
7
|
+
export const CONFIG_FILE = `${CONFIG_DIR}/config.json`;
|
|
8
|
+
export const LOG_FILE = `${CONFIG_DIR}/routstrd.log`;
|
|
9
|
+
|
|
10
|
+
export interface RoutstrdConfig {
|
|
11
|
+
port: number;
|
|
12
|
+
provider: string | null;
|
|
13
|
+
cocodPath: string | null;
|
|
14
|
+
mode?: "xcashu" | "apikeys";
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const DEFAULT_CONFIG: RoutstrdConfig = {
|
|
18
|
+
port: 8008,
|
|
19
|
+
provider: null,
|
|
20
|
+
cocodPath: null,
|
|
21
|
+
mode: "apikeys",
|
|
22
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { appendFile, mkdir } from "fs/promises";
|
|
2
|
+
import { existsSync } from "fs";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
|
|
5
|
+
const HOME = process.env.HOME || process.env.USERPROFILE || "";
|
|
6
|
+
const LOG_DIR = process.env.ROUTSTRD_DIR || `${HOME}/.routstrd`;
|
|
7
|
+
const LOG_FILE = join(LOG_DIR, "routstrd.log");
|
|
8
|
+
|
|
9
|
+
async function ensureLogDir() {
|
|
10
|
+
if (!existsSync(LOG_DIR)) {
|
|
11
|
+
await mkdir(LOG_DIR, { recursive: true });
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async function writeLog(level: string, ...args: unknown[]) {
|
|
16
|
+
await ensureLogDir();
|
|
17
|
+
const timestamp = new Date().toISOString();
|
|
18
|
+
const message = args
|
|
19
|
+
.map((a) => {
|
|
20
|
+
if (a instanceof Error) {
|
|
21
|
+
return `${a.message}${a.stack ? `\n${a.stack}` : ""}`;
|
|
22
|
+
}
|
|
23
|
+
if (typeof a === "object") {
|
|
24
|
+
try {
|
|
25
|
+
return JSON.stringify(a);
|
|
26
|
+
} catch {
|
|
27
|
+
return String(a);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return String(a);
|
|
31
|
+
})
|
|
32
|
+
.join(" ");
|
|
33
|
+
const line = `[${timestamp}] [${level}] ${message}\n`;
|
|
34
|
+
try {
|
|
35
|
+
await appendFile(LOG_FILE, line);
|
|
36
|
+
} catch (error) {
|
|
37
|
+
console.error("Failed to write log:", error);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const logger = {
|
|
42
|
+
log: (...args: unknown[]) => {
|
|
43
|
+
console.log(...args);
|
|
44
|
+
writeLog("INFO", ...args);
|
|
45
|
+
},
|
|
46
|
+
error: (...args: unknown[]) => {
|
|
47
|
+
console.error(...args);
|
|
48
|
+
writeLog("ERROR", ...args);
|
|
49
|
+
},
|
|
50
|
+
info: (...args: unknown[]) => {
|
|
51
|
+
console.log(...args);
|
|
52
|
+
writeLog("INFO", ...args);
|
|
53
|
+
},
|
|
54
|
+
};
|
package/test_box.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { renderBox } from "./src/tui/usage/render.ts";
|
|
2
|
+
import { stripAnsi } from "./src/tui/usage/terminal.ts";
|
|
3
|
+
|
|
4
|
+
const w = 40;
|
|
5
|
+
const testBox = renderBox(["Hello World", "Line 2"], w, "Title");
|
|
6
|
+
console.log(testBox);
|
|
7
|
+
const lines = testBox.split("\n");
|
|
8
|
+
lines.forEach((l, i) => console.log(`Line ${i} length: ${stripAnsi(l).length}`));
|
|
9
|
+
|
|
10
|
+
console.log("---");
|
|
11
|
+
|
|
12
|
+
const testBoxNoTitle = renderBox(["Hello World", "Line 2"], w);
|
|
13
|
+
console.log(testBoxNoTitle);
|
|
14
|
+
const lines2 = testBoxNoTitle.split("\n");
|
|
15
|
+
lines2.forEach((l, i) => console.log(`Line ${i} length: ${stripAnsi(l).length}`));
|
package/test_curl.sh
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
curl -X POST "http://localhost:8008/v1/chat/completions" \
|
|
3
|
+
-H "Authorization: Bearer YOUR_API_KEY" \
|
|
4
|
+
-H "Content-Type: application/json" \
|
|
5
|
+
-d '{
|
|
6
|
+
"model": "gemma-3n-e4b-it",
|
|
7
|
+
"messages": [
|
|
8
|
+
{"role":"system","content":"You are Routstr."},
|
|
9
|
+
{"role":"user","content":"Ping the node"}
|
|
10
|
+
]
|
|
11
|
+
}'
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { renderBox } from "./src/tui/usage/render.ts";
|
|
2
|
+
|
|
3
|
+
const width = 80;
|
|
4
|
+
const halfWidth1 = Math.floor(width / 2);
|
|
5
|
+
const halfWidth2 = width - halfWidth1;
|
|
6
|
+
|
|
7
|
+
const leftBox = ["Total Spent: 12.78k sats", "Total Requests: 1.0k"];
|
|
8
|
+
const rightBox = ["Total Tokens: 25.8M", "Avg Tokens/Req: 25.8K"];
|
|
9
|
+
|
|
10
|
+
const leftBoxStr = renderBox(leftBox, halfWidth1, "Stats of Sats");
|
|
11
|
+
const rightBoxStr = renderBox(rightBox, halfWidth2, "Token Stats");
|
|
12
|
+
|
|
13
|
+
const leftLines = leftBoxStr.split("\n");
|
|
14
|
+
const rightLines = rightBoxStr.split("\n");
|
|
15
|
+
|
|
16
|
+
const combinedContent = leftLines.map((l, i) => l + (rightLines[i] || " ".repeat(halfWidth2))).join("\n");
|
|
17
|
+
console.log(combinedContent);
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { renderBox } from "./src/tui/usage/render.ts";
|
|
2
|
+
|
|
3
|
+
const width = 80;
|
|
4
|
+
const halfWidth1 = Math.floor(width / 2);
|
|
5
|
+
const halfWidth2 = width - halfWidth1;
|
|
6
|
+
|
|
7
|
+
const leftBox = ["Total Spent: 12.78k sats", "Total Requests: 1.0k"];
|
|
8
|
+
const rightBox = ["Total Tokens: 25.8M", "Avg Tokens/Req: 25.8K"];
|
|
9
|
+
|
|
10
|
+
const leftBoxStr = renderBox(leftBox, halfWidth1, "Stats of Sats");
|
|
11
|
+
const rightBoxStr = renderBox(rightBox, halfWidth2, "Token Stats");
|
|
12
|
+
|
|
13
|
+
const leftLines = leftBoxStr.split("\n");
|
|
14
|
+
const rightLines = rightBoxStr.split("\n");
|
|
15
|
+
|
|
16
|
+
const maxLines = Math.max(leftLines.length, rightLines.length);
|
|
17
|
+
const combinedLines: string[] = [];
|
|
18
|
+
for (let i = 0; i < maxLines; i++) {
|
|
19
|
+
const l = leftLines[i] || " ".repeat(Math.floor(width / 2));
|
|
20
|
+
const r = rightLines[i] || " ".repeat(Math.ceil(width / 2));
|
|
21
|
+
combinedLines.push(l + r);
|
|
22
|
+
}
|
|
23
|
+
console.log(combinedLines.join("\n"));
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"lib": ["ESNext"],
|
|
4
|
+
"target": "ESNext",
|
|
5
|
+
"module": "ESNext",
|
|
6
|
+
"moduleResolution": "bundler",
|
|
7
|
+
"strict": true,
|
|
8
|
+
"noUncheckedIndexedAccess": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"forceConsistentCasingInFileNames": true,
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"isolatedModules": true,
|
|
14
|
+
"allowImportingTsExtensions": true,
|
|
15
|
+
"noEmit": true,
|
|
16
|
+
"types": ["bun-types"]
|
|
17
|
+
},
|
|
18
|
+
"include": ["src/**/*"],
|
|
19
|
+
"exclude": ["node_modules"]
|
|
20
|
+
}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
# Report: why `POST /v1/messages` on port 8008 returns chat-completions chunks while port 8009 preserves Messages API format
|
|
2
|
+
|
|
3
|
+
Date: 2026-03-28
|
|
4
|
+
|
|
5
|
+
## Summary
|
|
6
|
+
|
|
7
|
+
I tested the same request against both local daemons using `scripts/test-direct-local.ts` semantics (`POST /v1/messages`, `stream: true`).
|
|
8
|
+
|
|
9
|
+
- `localhost:8009` preserves the Anthropic/OpenAI Messages-style stream as expected:
|
|
10
|
+
- `message_start`
|
|
11
|
+
- `content_block_start`
|
|
12
|
+
- `content_block_delta`
|
|
13
|
+
- `message_delta`
|
|
14
|
+
- `message_stop`
|
|
15
|
+
- `localhost:8008` returns OpenAI chat-completions streaming chunks instead:
|
|
16
|
+
- `object: "chat.completion.chunk"`
|
|
17
|
+
- `choices[].delta`
|
|
18
|
+
|
|
19
|
+
This is **not** because the SDK itself is incapable of preserving `/v1/messages`.
|
|
20
|
+
It happens because the two daemons call the SDK differently.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## What I tested
|
|
25
|
+
|
|
26
|
+
### 8008 (`../routstrd/`)
|
|
27
|
+
|
|
28
|
+
Listener:
|
|
29
|
+
- `../routstrd/src/daemon/index.ts`
|
|
30
|
+
- request handler in `../routstrd/src/daemon/http/index.ts`
|
|
31
|
+
|
|
32
|
+
Observed result for `POST http://localhost:8008/v1/messages`:
|
|
33
|
+
- response `content-type: text/event-stream`
|
|
34
|
+
- SSE payload is OpenAI chat-completions chunks (`chat.completion.chunk`)
|
|
35
|
+
|
|
36
|
+
### 8009 (`routstr-chat/scripts/routstr-daemon.ts`)
|
|
37
|
+
|
|
38
|
+
Listener:
|
|
39
|
+
- `scripts/routstr-daemon.ts`
|
|
40
|
+
|
|
41
|
+
Observed result for `POST http://localhost:8009/v1/messages`:
|
|
42
|
+
- response `content-type: text/event-stream`
|
|
43
|
+
- SSE payload preserves Messages API events (`message_start`, `content_block_delta`, etc.)
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Direct cause
|
|
48
|
+
|
|
49
|
+
### 8009 forwards the incoming path to the SDK
|
|
50
|
+
|
|
51
|
+
In `routstr-chat/scripts/routstr-daemon.ts`, the request is routed with:
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
await routeRequestsToNodeResponse({
|
|
55
|
+
modelId,
|
|
56
|
+
requestBody,
|
|
57
|
+
path: url.pathname,
|
|
58
|
+
headers: forwardedHeaders,
|
|
59
|
+
...
|
|
60
|
+
});
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Because it passes:
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
path: url.pathname
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
an incoming request to `/v1/messages` stays `/v1/messages` all the way through the SDK and upstream provider routing.
|
|
70
|
+
|
|
71
|
+
### 8008 does **not** forward the incoming path
|
|
72
|
+
|
|
73
|
+
In `../routstrd/src/daemon/http/index.ts`, the request is routed with:
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
const response = await routeRequests({
|
|
77
|
+
modelId,
|
|
78
|
+
requestBody,
|
|
79
|
+
forcedProvider,
|
|
80
|
+
headers: incomingHeaders,
|
|
81
|
+
walletAdapter: deps.walletAdapter,
|
|
82
|
+
storageAdapter: deps.storageAdapter,
|
|
83
|
+
providerRegistry: deps.providerRegistry,
|
|
84
|
+
discoveryAdapter: deps.discoveryAdapter,
|
|
85
|
+
modelManager: deps.modelManager,
|
|
86
|
+
debugLevel: "DEBUG",
|
|
87
|
+
mode: deps.mode,
|
|
88
|
+
usageTrackingDriver: deps.usageTrackingDriver,
|
|
89
|
+
sdkStore: deps.store,
|
|
90
|
+
});
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Notice: **no `path` is passed**.
|
|
94
|
+
|
|
95
|
+
In the SDK, `routeRequests()` defaults the path to:
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
path = "/v1/chat/completions"
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
from:
|
|
102
|
+
- `routstr-chat/sdk/routeRequests.ts`
|
|
103
|
+
|
|
104
|
+
So even when the client calls:
|
|
105
|
+
|
|
106
|
+
```http
|
|
107
|
+
POST /v1/messages
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
on port 8008, the daemon internally re-routes it as:
|
|
111
|
+
|
|
112
|
+
```http
|
|
113
|
+
POST /v1/chat/completions
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
That is why the upstream/provider response is converted into chat-completions format.
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## Why the behavior differs even though both are built on the same SDK
|
|
121
|
+
|
|
122
|
+
Both daemons use the same SDK primitives, but:
|
|
123
|
+
|
|
124
|
+
- **8009** uses `routeRequestsToNodeResponse(...)` and explicitly passes `path: url.pathname`
|
|
125
|
+
- **8008** uses `routeRequests(...)` and relies on the SDK default path, which is `/v1/chat/completions`
|
|
126
|
+
|
|
127
|
+
So the format difference is caused by **daemon integration code**, not by a provider-specific quirk and not by an unavoidable SDK conversion.
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## Important implementation detail
|
|
132
|
+
|
|
133
|
+
The SDK helper itself documents this default:
|
|
134
|
+
|
|
135
|
+
- `routstr-chat/sdk/routeRequests.ts`
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
/** Optional: API path (defaults to /v1/chat/completions) */
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
and in `resolveRouteRequestContext(...)`:
|
|
142
|
+
|
|
143
|
+
```ts
|
|
144
|
+
path = "/v1/chat/completions"
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Therefore any caller that omits `path` will get chat-completions semantics by default.
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
## Evidence from runtime tests
|
|
152
|
+
|
|
153
|
+
### Port 8008
|
|
154
|
+
|
|
155
|
+
Observed streamed chunks included:
|
|
156
|
+
|
|
157
|
+
- `object: "chat.completion.chunk"`
|
|
158
|
+
- `choices[0].delta.content`
|
|
159
|
+
- final `[DONE]`
|
|
160
|
+
|
|
161
|
+
### Port 8009
|
|
162
|
+
|
|
163
|
+
Observed streamed events included:
|
|
164
|
+
|
|
165
|
+
- `event: message_start`
|
|
166
|
+
- `event: content_block_start`
|
|
167
|
+
- `event: content_block_delta`
|
|
168
|
+
- `event: message_delta`
|
|
169
|
+
- `event: message_stop`
|
|
170
|
+
- final `[DONE]`
|
|
171
|
+
|
|
172
|
+
This matches the code-path difference above.
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
## Recommended fix
|
|
177
|
+
|
|
178
|
+
In `../routstrd/src/daemon/http/index.ts`, pass the incoming path through to the SDK:
|
|
179
|
+
|
|
180
|
+
```ts
|
|
181
|
+
const response = await routeRequests({
|
|
182
|
+
modelId,
|
|
183
|
+
requestBody,
|
|
184
|
+
path: url.pathname,
|
|
185
|
+
forcedProvider,
|
|
186
|
+
headers: incomingHeaders,
|
|
187
|
+
walletAdapter: deps.walletAdapter,
|
|
188
|
+
storageAdapter: deps.storageAdapter,
|
|
189
|
+
providerRegistry: deps.providerRegistry,
|
|
190
|
+
discoveryAdapter: deps.discoveryAdapter,
|
|
191
|
+
modelManager: deps.modelManager,
|
|
192
|
+
debugLevel: "DEBUG",
|
|
193
|
+
mode: deps.mode,
|
|
194
|
+
usageTrackingDriver: deps.usageTrackingDriver,
|
|
195
|
+
sdkStore: deps.store,
|
|
196
|
+
});
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
This should make `POST /v1/messages` on port 8008 preserve the Messages API format, matching port 8009.
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
## Secondary note
|
|
204
|
+
|
|
205
|
+
Port 8008 currently uses `routeRequests(...)` and then manually streams the returned response body to `res`.
|
|
206
|
+
Port 8009 uses `routeRequestsToNodeResponse(...)` directly.
|
|
207
|
+
|
|
208
|
+
That difference is probably not the root cause here.
|
|
209
|
+
The root cause is specifically that **8008 drops the incoming request path and falls back to the SDK default of `/v1/chat/completions`**.
|
|
210
|
+
|
|
211
|
+
---
|
|
212
|
+
|
|
213
|
+
## Conclusion
|
|
214
|
+
|
|
215
|
+
The reason `localhost:8008/v1/messages` appears to "convert to chat completions" is:
|
|
216
|
+
|
|
217
|
+
1. `../routstrd` receives `/v1/messages`
|
|
218
|
+
2. its handler calls `routeRequests(...)` without `path`
|
|
219
|
+
3. the SDK defaults `path` to `/v1/chat/completions`
|
|
220
|
+
4. the upstream request is therefore made against chat completions
|
|
221
|
+
5. the stream returned is chat-completions SSE, not Messages API SSE
|
|
222
|
+
|
|
223
|
+
Port 8009 works because it explicitly forwards `url.pathname` into the SDK call.
|