@cyanlabs/t3chat 0.0.1
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 +24 -0
- package/dist/cli +41 -0
- package/package.json +27 -0
- package/src/cli.ts +56 -0
- package/src/commands/auth/index.ts +12 -0
- package/src/commands/auth/login.ts +42 -0
- package/src/commands/models.ts +37 -0
- package/src/lib/config.ts +60 -0
- package/src/lib/constants.ts +44 -0
- package/src/lib/t3client.ts +309 -0
- package/tsconfig.json +17 -0
package/README.md
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# t3chat-cli
|
|
2
|
+
|
|
3
|
+
A CLI built with [Crust](https://crustjs.com).
|
|
4
|
+
|
|
5
|
+
## Development
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
# Run in dev mode
|
|
9
|
+
bun run dev
|
|
10
|
+
|
|
11
|
+
# Type-check
|
|
12
|
+
bun run check:types
|
|
13
|
+
|
|
14
|
+
# Build standalone executable
|
|
15
|
+
bun run build
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Usage
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
# Run the CLI
|
|
22
|
+
t3chat-cli world
|
|
23
|
+
t3chat-cli --greet Hey world
|
|
24
|
+
```
|
package/dist/cli
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Auto-generated by crust build — do not edit
|
|
3
|
+
set -e
|
|
4
|
+
|
|
5
|
+
source="$0"
|
|
6
|
+
while [ -L "$source" ]; do
|
|
7
|
+
link_dir="$(cd "$(dirname "$source")" && pwd)"
|
|
8
|
+
source="$(readlink "$source")"
|
|
9
|
+
# Resolve relative symlinks
|
|
10
|
+
[[ "$source" != /* ]] && source="$link_dir/$source"
|
|
11
|
+
done
|
|
12
|
+
dir="$(cd "$(dirname "$source")" && pwd)"
|
|
13
|
+
platform="$(uname -s)-$(uname -m)"
|
|
14
|
+
|
|
15
|
+
case "$platform" in
|
|
16
|
+
Linux-x86_64) bin="t3chat-cli-bun-linux-x64-baseline" ;;
|
|
17
|
+
Linux-aarch64) bin="t3chat-cli-bun-linux-arm64" ;;
|
|
18
|
+
Darwin-x86_64) bin="t3chat-cli-bun-darwin-x64" ;;
|
|
19
|
+
Darwin-arm64) bin="t3chat-cli-bun-darwin-arm64" ;;
|
|
20
|
+
*)
|
|
21
|
+
echo "[t3chat-cli] Unsupported platform: $platform" >&2
|
|
22
|
+
echo "[t3chat-cli] Please open an issue with your OS/CPU details." >&2
|
|
23
|
+
exit 1
|
|
24
|
+
;;
|
|
25
|
+
esac
|
|
26
|
+
|
|
27
|
+
bin_path="$dir/$bin"
|
|
28
|
+
|
|
29
|
+
if [ ! -f "$bin_path" ]; then
|
|
30
|
+
echo "[t3chat-cli] Prebuilt binary not found: $bin_path" >&2
|
|
31
|
+
echo "[t3chat-cli] Expected binary for $platform: $bin" >&2
|
|
32
|
+
echo "[t3chat-cli] Try reinstalling the package." >&2
|
|
33
|
+
exit 1
|
|
34
|
+
fi
|
|
35
|
+
|
|
36
|
+
# Ensure the binary is executable
|
|
37
|
+
if [ ! -x "$bin_path" ]; then
|
|
38
|
+
chmod +x "$bin_path" 2>/dev/null || true
|
|
39
|
+
fi
|
|
40
|
+
|
|
41
|
+
exec "$bin_path" "$@"
|
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@cyanlabs/t3chat",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "CLI wrapper for t3.chat",
|
|
6
|
+
"bin": {
|
|
7
|
+
"chat": "dist/cli"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"dev": "bun run src/cli.ts",
|
|
11
|
+
"build": "crust build",
|
|
12
|
+
"start": "./dist/cli",
|
|
13
|
+
"check:types": "tsc --noEmit"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@crustjs/core": "^0.0.6",
|
|
17
|
+
"@crustjs/plugins": "^0.0.7",
|
|
18
|
+
"@crustjs/prompts": "^0.0.3"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@changesets/changelog-github": "^0.5.2",
|
|
22
|
+
"@changesets/cli": "^2.29.8",
|
|
23
|
+
"@crustjs/crust": "^0.0.10",
|
|
24
|
+
"@types/bun": "latest",
|
|
25
|
+
"typescript": "^5.9.3"
|
|
26
|
+
}
|
|
27
|
+
}
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { defineCommand, runMain } from "@crustjs/core";
|
|
2
|
+
import { helpPlugin, renderHelp, versionPlugin } from "@crustjs/plugins";
|
|
3
|
+
import pkg from "../package.json";
|
|
4
|
+
import { auth } from "./commands/auth/index.ts";
|
|
5
|
+
import { models } from "./commands/models.ts";
|
|
6
|
+
import { getCookies, getModel } from "./lib/config.ts";
|
|
7
|
+
import { formatModelName } from "./lib/constants.ts";
|
|
8
|
+
import { sendMessage } from "./lib/t3client.ts";
|
|
9
|
+
|
|
10
|
+
const main = defineCommand({
|
|
11
|
+
meta: {
|
|
12
|
+
name: "chat",
|
|
13
|
+
description: "CLI wrapper for t3.chat",
|
|
14
|
+
},
|
|
15
|
+
args: [
|
|
16
|
+
{
|
|
17
|
+
name: "prompt",
|
|
18
|
+
type: "string",
|
|
19
|
+
variadic: true,
|
|
20
|
+
description: "The message to send to t3.chat",
|
|
21
|
+
},
|
|
22
|
+
],
|
|
23
|
+
subCommands: {
|
|
24
|
+
auth,
|
|
25
|
+
models,
|
|
26
|
+
},
|
|
27
|
+
async run({ args }) {
|
|
28
|
+
if (args.prompt.length <= 0) {
|
|
29
|
+
console.log(renderHelp(main));
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const prompt = args.prompt.join(" ");
|
|
34
|
+
|
|
35
|
+
const cookies = getCookies();
|
|
36
|
+
if (!cookies) {
|
|
37
|
+
console.error("Not authenticated. Run `chat auth login` first.");
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const modelId = getModel();
|
|
42
|
+
console.log(`Model: ${formatModelName(modelId)}\n`);
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
await sendMessage(prompt, modelId);
|
|
46
|
+
} catch (e) {
|
|
47
|
+
const err = e as Error;
|
|
48
|
+
console.error(`Error: ${err.message}`);
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
runMain(main, {
|
|
55
|
+
plugins: [versionPlugin(pkg.version), helpPlugin()],
|
|
56
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { defineCommand } from "@crustjs/core";
|
|
2
|
+
import { input } from "@crustjs/prompts";
|
|
3
|
+
import { loadConfig, saveConfig } from "../../lib/config.ts";
|
|
4
|
+
import { T3_CHAT_URL } from "../../lib/constants.ts";
|
|
5
|
+
|
|
6
|
+
export const login = defineCommand({
|
|
7
|
+
meta: {
|
|
8
|
+
name: "login",
|
|
9
|
+
description: "Authenticate with t3.chat by providing your cookies",
|
|
10
|
+
},
|
|
11
|
+
async run() {
|
|
12
|
+
console.log("Opening t3.chat in your browser...\n");
|
|
13
|
+
|
|
14
|
+
// Open browser (macOS)
|
|
15
|
+
Bun.spawn(["open", T3_CHAT_URL]);
|
|
16
|
+
|
|
17
|
+
console.log("To get your cookies:");
|
|
18
|
+
console.log(" 1. Log in to t3.chat in the browser");
|
|
19
|
+
console.log(" 2. Open DevTools (Cmd+Option+I)");
|
|
20
|
+
console.log(" 3. Go to Network tab");
|
|
21
|
+
console.log(" 4. Send any chat message");
|
|
22
|
+
console.log(" 5. Click the /api/chat request");
|
|
23
|
+
console.log(" 6. In the Headers tab, find the 'cookie' request header");
|
|
24
|
+
console.log(" 7. Copy the entire cookie string\n");
|
|
25
|
+
|
|
26
|
+
const cookies = await input({
|
|
27
|
+
message: "Paste your cookie string:",
|
|
28
|
+
validate: (value) => {
|
|
29
|
+
if (!value.trim()) return "Cookie string cannot be empty";
|
|
30
|
+
if (!value.includes("convex-session-id")) {
|
|
31
|
+
return "Cookie should contain 'convex-session-id'. Make sure you copied the full cookie header.";
|
|
32
|
+
}
|
|
33
|
+
return true;
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const config = loadConfig();
|
|
38
|
+
config.cookies = cookies.trim();
|
|
39
|
+
saveConfig(config);
|
|
40
|
+
console.log("\nCookies saved. You can now use `chat <prompt>` to chat.");
|
|
41
|
+
},
|
|
42
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { defineCommand } from "@crustjs/core";
|
|
2
|
+
import { filter, spinner } from "@crustjs/prompts";
|
|
3
|
+
import { loadConfig, saveConfig } from "../lib/config.ts";
|
|
4
|
+
import { DEFAULT_MODEL_ID } from "../lib/constants.ts";
|
|
5
|
+
import { fetchModels } from "../lib/t3client.ts";
|
|
6
|
+
|
|
7
|
+
export const models = defineCommand({
|
|
8
|
+
meta: {
|
|
9
|
+
name: "models",
|
|
10
|
+
description: "Select the default model for chat",
|
|
11
|
+
},
|
|
12
|
+
async run() {
|
|
13
|
+
const config = loadConfig();
|
|
14
|
+
const currentModel = config.model ?? DEFAULT_MODEL_ID;
|
|
15
|
+
|
|
16
|
+
const modelList = await spinner({
|
|
17
|
+
message: "Fetching available models...",
|
|
18
|
+
task: () => fetchModels(),
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
console.log(`Current model: ${currentModel}\n`);
|
|
22
|
+
|
|
23
|
+
const selected = await filter<string>({
|
|
24
|
+
message: "Choose a model:",
|
|
25
|
+
placeholder: "Type to filter...",
|
|
26
|
+
choices: modelList.map((m) => ({
|
|
27
|
+
label: `${m.name} (${m.provider}) ${m.cost}`,
|
|
28
|
+
value: m.id,
|
|
29
|
+
})),
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
config.model = selected;
|
|
33
|
+
saveConfig(config);
|
|
34
|
+
const model = modelList.find((m) => m.id === selected);
|
|
35
|
+
console.log(`\nDefault model set to: ${model?.name ?? selected}`);
|
|
36
|
+
},
|
|
37
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { CONFIG_DIR_NAME, DEFAULT_MODEL_ID } from "./constants.ts";
|
|
5
|
+
|
|
6
|
+
export interface Config {
|
|
7
|
+
/** All cookies from t3.chat as a single string (cookie header format) */
|
|
8
|
+
cookies?: string;
|
|
9
|
+
/** Selected model ID */
|
|
10
|
+
model?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function getConfigDir(): string {
|
|
14
|
+
const dir = join(homedir(), ".config", CONFIG_DIR_NAME);
|
|
15
|
+
if (!existsSync(dir)) {
|
|
16
|
+
mkdirSync(dir, { recursive: true });
|
|
17
|
+
}
|
|
18
|
+
return dir;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function getConfigPath(): string {
|
|
22
|
+
return join(getConfigDir(), "config.json");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function loadConfig(): Config {
|
|
26
|
+
const path = getConfigPath();
|
|
27
|
+
if (!existsSync(path)) {
|
|
28
|
+
return {};
|
|
29
|
+
}
|
|
30
|
+
try {
|
|
31
|
+
const raw = readFileSync(path, "utf-8");
|
|
32
|
+
return JSON.parse(raw) as Config;
|
|
33
|
+
} catch {
|
|
34
|
+
return {};
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function saveConfig(config: Config): void {
|
|
39
|
+
const path = getConfigPath();
|
|
40
|
+
writeFileSync(path, `${JSON.stringify(config, null, 2)}\n`, "utf-8");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function getCookies(): string | undefined {
|
|
44
|
+
return loadConfig().cookies;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function getModel(): string {
|
|
48
|
+
return loadConfig().model ?? DEFAULT_MODEL_ID;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Extract the convex-session-id value from the stored cookie string.
|
|
53
|
+
* This is needed in the request body as `convexSessionId`.
|
|
54
|
+
*/
|
|
55
|
+
export function getConvexSessionId(): string | undefined {
|
|
56
|
+
const cookies = getCookies();
|
|
57
|
+
if (!cookies) return undefined;
|
|
58
|
+
const match = cookies.match(/convex-session-id=([^;]+)/);
|
|
59
|
+
return match?.[1];
|
|
60
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export const T3_CHAT_URL = "https://t3.chat";
|
|
2
|
+
export const CHAT_API_URL = "https://t3.chat/api/chat";
|
|
3
|
+
export const TRPC_API_URL = "https://t3.chat/api/trpc";
|
|
4
|
+
// Config paths
|
|
5
|
+
export const CONFIG_DIR_NAME = "t3chat-cli";
|
|
6
|
+
|
|
7
|
+
export interface Model {
|
|
8
|
+
id: string;
|
|
9
|
+
name: string;
|
|
10
|
+
provider: string;
|
|
11
|
+
cost: "$" | "$$" | "$$$";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Derive provider name from model ID prefix */
|
|
15
|
+
export function getProviderFromModelId(id: string): string {
|
|
16
|
+
if (id.startsWith("claude-")) return "Anthropic";
|
|
17
|
+
if (id.startsWith("gpt-") || id.startsWith("o3-")) return "OpenAI";
|
|
18
|
+
if (id.startsWith("gemini-")) return "Google";
|
|
19
|
+
if (id.startsWith("deepseek-")) return "DeepSeek";
|
|
20
|
+
if (id.startsWith("kimi-")) return "Moonshot";
|
|
21
|
+
if (id.startsWith("llama-")) return "Meta";
|
|
22
|
+
if (id.startsWith("minimax-")) return "MiniMax";
|
|
23
|
+
if (id.startsWith("grok-")) return "xAI";
|
|
24
|
+
if (id.startsWith("glm-")) return "Zhipu";
|
|
25
|
+
if (id.startsWith("qwen")) return "Alibaba";
|
|
26
|
+
return "Unknown";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Map blended price per 1M tokens to a cost tier */
|
|
30
|
+
export function getCostTier(blendedPrice: number): "$" | "$$" | "$$$" {
|
|
31
|
+
if (blendedPrice < 1) return "$";
|
|
32
|
+
if (blendedPrice <= 5) return "$$";
|
|
33
|
+
return "$$$";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Format a model ID into a human-readable name */
|
|
37
|
+
export function formatModelName(id: string): string {
|
|
38
|
+
return id
|
|
39
|
+
.split("-")
|
|
40
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
41
|
+
.join(" ");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export const DEFAULT_MODEL_ID = "kimi-k2.5";
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import {
|
|
3
|
+
CHAT_API_URL,
|
|
4
|
+
TRPC_API_URL,
|
|
5
|
+
T3_CHAT_URL,
|
|
6
|
+
type Model,
|
|
7
|
+
getProviderFromModelId,
|
|
8
|
+
getCostTier,
|
|
9
|
+
formatModelName,
|
|
10
|
+
} from "./constants.ts";
|
|
11
|
+
import { getCookies, getConvexSessionId, getModel } from "./config.ts";
|
|
12
|
+
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// Shared curl helpers
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
/** Browser-like headers required to bypass Vercel bot protection */
|
|
18
|
+
function browserHeaders(cookies: string): string[] {
|
|
19
|
+
return [
|
|
20
|
+
"-H",
|
|
21
|
+
"Accept: */*",
|
|
22
|
+
"-H",
|
|
23
|
+
"Accept-Language: en-US,en;q=0.9",
|
|
24
|
+
"-H",
|
|
25
|
+
`Cookie: ${cookies}`,
|
|
26
|
+
"-H",
|
|
27
|
+
`Origin: ${T3_CHAT_URL}`,
|
|
28
|
+
"-H",
|
|
29
|
+
`Referer: ${T3_CHAT_URL}/`,
|
|
30
|
+
"-H",
|
|
31
|
+
"User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36",
|
|
32
|
+
"-H",
|
|
33
|
+
'sec-ch-ua: "Not:A-Brand";v="99", "Google Chrome";v="145", "Chromium";v="145"',
|
|
34
|
+
"-H",
|
|
35
|
+
"sec-ch-ua-mobile: ?0",
|
|
36
|
+
"-H",
|
|
37
|
+
'sec-ch-ua-platform: "macOS"',
|
|
38
|
+
"-H",
|
|
39
|
+
"Sec-Fetch-Dest: empty",
|
|
40
|
+
"-H",
|
|
41
|
+
"Sec-Fetch-Mode: cors",
|
|
42
|
+
"-H",
|
|
43
|
+
"Sec-Fetch-Site: same-origin",
|
|
44
|
+
];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Run curl and return the response body + HTTP status code */
|
|
48
|
+
async function curlRequest(
|
|
49
|
+
args: string[],
|
|
50
|
+
): Promise<{ body: string; httpStatus: number }> {
|
|
51
|
+
const proc = Bun.spawn(
|
|
52
|
+
[
|
|
53
|
+
"curl",
|
|
54
|
+
"--silent",
|
|
55
|
+
"--show-error",
|
|
56
|
+
"-w",
|
|
57
|
+
"\n__HTTP_STATUS__%{http_code}",
|
|
58
|
+
...args,
|
|
59
|
+
],
|
|
60
|
+
{ stdout: "pipe", stderr: "pipe" },
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
const stdoutText = await new Response(proc.stdout).text();
|
|
64
|
+
await proc.exited;
|
|
65
|
+
|
|
66
|
+
const statusMatch = stdoutText.match(/__HTTP_STATUS__(\d+)$/);
|
|
67
|
+
const httpStatus = statusMatch ? Number(statusMatch[1]) : 0;
|
|
68
|
+
const body = statusMatch
|
|
69
|
+
? stdoutText.slice(0, statusMatch.index)
|
|
70
|
+
: stdoutText;
|
|
71
|
+
|
|
72
|
+
return { body, httpStatus };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function requireCookies(): string {
|
|
76
|
+
const cookies = getCookies();
|
|
77
|
+
if (!cookies) {
|
|
78
|
+
throw new Error("Not authenticated. Run `chat auth login` first.");
|
|
79
|
+
}
|
|
80
|
+
return cookies;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
// Fetch models via tRPC
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
|
|
87
|
+
interface TrpcModelBenchmark {
|
|
88
|
+
slug: string;
|
|
89
|
+
intelligenceIndex: number | null;
|
|
90
|
+
codingIndex: number | null;
|
|
91
|
+
mathIndex: number | null;
|
|
92
|
+
pricing: {
|
|
93
|
+
price_1m_blended_3_to_1: number;
|
|
94
|
+
price_1m_input_tokens: number;
|
|
95
|
+
price_1m_output_tokens: number;
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Fetch available models from t3.chat's tRPC endpoint.
|
|
101
|
+
*
|
|
102
|
+
* Calls GET /api/trpc/getAllModelBenchmarks which returns a map of
|
|
103
|
+
* model IDs → benchmark + pricing data. We transform this into our
|
|
104
|
+
* Model[] format.
|
|
105
|
+
*/
|
|
106
|
+
export async function fetchModels(): Promise<Model[]> {
|
|
107
|
+
const cookies = requireCookies();
|
|
108
|
+
|
|
109
|
+
const input = encodeURIComponent(
|
|
110
|
+
JSON.stringify({ json: null, meta: { values: ["undefined"] } }),
|
|
111
|
+
);
|
|
112
|
+
const url = `${TRPC_API_URL}/getAllModelBenchmarks?input=${input}`;
|
|
113
|
+
|
|
114
|
+
const { body, httpStatus } = await curlRequest([
|
|
115
|
+
url,
|
|
116
|
+
...browserHeaders(cookies),
|
|
117
|
+
]);
|
|
118
|
+
|
|
119
|
+
if (httpStatus !== 200) {
|
|
120
|
+
throw new Error(`tRPC API returned HTTP ${httpStatus}`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Parse the tRPC response.
|
|
124
|
+
// Non-batched format: { result: { data: { json: { ...models }, meta: { ... } } } }
|
|
125
|
+
// Batched format: [{ result: { data: { json: { ...models } } } }]
|
|
126
|
+
const response = JSON.parse(body);
|
|
127
|
+
|
|
128
|
+
let modelMap: Record<string, TrpcModelBenchmark> | undefined;
|
|
129
|
+
|
|
130
|
+
if (Array.isArray(response)) {
|
|
131
|
+
// Batched response — find the first entry with model data
|
|
132
|
+
for (const item of response) {
|
|
133
|
+
const candidate = item?.result?.data?.json;
|
|
134
|
+
if (
|
|
135
|
+
candidate &&
|
|
136
|
+
typeof candidate === "object" &&
|
|
137
|
+
!Array.isArray(candidate)
|
|
138
|
+
) {
|
|
139
|
+
modelMap = candidate;
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
} else {
|
|
144
|
+
modelMap = response?.result?.data?.json;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (!modelMap || typeof modelMap !== "object") {
|
|
148
|
+
throw new Error("Unexpected response format from model benchmarks API");
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return Object.entries(modelMap)
|
|
152
|
+
.map(([id, data]) => ({
|
|
153
|
+
id,
|
|
154
|
+
name: formatModelName(id),
|
|
155
|
+
provider: getProviderFromModelId(id),
|
|
156
|
+
cost: getCostTier(data.pricing.price_1m_blended_3_to_1),
|
|
157
|
+
}))
|
|
158
|
+
.sort((a, b) => {
|
|
159
|
+
if (a.provider !== b.provider)
|
|
160
|
+
return a.provider.localeCompare(b.provider);
|
|
161
|
+
return a.id.localeCompare(b.id);
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ---------------------------------------------------------------------------
|
|
166
|
+
// Chat
|
|
167
|
+
// ---------------------------------------------------------------------------
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Build the request body matching t3.chat's expected format.
|
|
171
|
+
*
|
|
172
|
+
* The payload was reverse-engineered from the browser's /api/chat request.
|
|
173
|
+
* All top-level fields appear to be validated server-side.
|
|
174
|
+
*/
|
|
175
|
+
function buildRequestBody(
|
|
176
|
+
prompt: string,
|
|
177
|
+
model: string,
|
|
178
|
+
sessionId: string,
|
|
179
|
+
) {
|
|
180
|
+
return {
|
|
181
|
+
messages: [
|
|
182
|
+
{
|
|
183
|
+
id: randomUUID(),
|
|
184
|
+
role: "user",
|
|
185
|
+
parts: [{ type: "text", text: prompt }],
|
|
186
|
+
attachments: [],
|
|
187
|
+
},
|
|
188
|
+
],
|
|
189
|
+
threadMetadata: {
|
|
190
|
+
id: randomUUID(),
|
|
191
|
+
title: "",
|
|
192
|
+
},
|
|
193
|
+
responseMessageId: randomUUID(),
|
|
194
|
+
model,
|
|
195
|
+
convexSessionId: sessionId,
|
|
196
|
+
modelParams: {
|
|
197
|
+
reasoningEffort: "low",
|
|
198
|
+
includeSearch: false,
|
|
199
|
+
searchLimit: 1,
|
|
200
|
+
},
|
|
201
|
+
preferences: {
|
|
202
|
+
name: "",
|
|
203
|
+
occupation: "",
|
|
204
|
+
selectedTraits: [],
|
|
205
|
+
additionalInfo: "",
|
|
206
|
+
},
|
|
207
|
+
userConfiguration: {
|
|
208
|
+
currentlySelectedModel: model,
|
|
209
|
+
currentModelParameters: {
|
|
210
|
+
includeSearch: false,
|
|
211
|
+
reasoningEffort: "low",
|
|
212
|
+
},
|
|
213
|
+
hasMigrated: true,
|
|
214
|
+
mainFont: "proxima",
|
|
215
|
+
streamerMode: false,
|
|
216
|
+
favoriteModels: [],
|
|
217
|
+
billingEmailsEnabled: true,
|
|
218
|
+
statsForNerds: false,
|
|
219
|
+
disableExternalLinkWarning: false,
|
|
220
|
+
disableHorizontalLines: false,
|
|
221
|
+
},
|
|
222
|
+
userInfo: {
|
|
223
|
+
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
224
|
+
locale: "en-US",
|
|
225
|
+
},
|
|
226
|
+
isEphemeral: true,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Send a chat message to t3.chat via POST /api/chat using curl.
|
|
232
|
+
*
|
|
233
|
+
* We use curl instead of fetch because Vercel's bot protection checks
|
|
234
|
+
* TLS fingerprints (JA3/JA4). Bun's fetch has a non-browser fingerprint
|
|
235
|
+
* that gets blocked with a 403. curl's TLS fingerprint is typically allowed.
|
|
236
|
+
*
|
|
237
|
+
* The response is SSE (Server-Sent Events) which we parse and stream
|
|
238
|
+
* text deltas to stdout.
|
|
239
|
+
*/
|
|
240
|
+
export async function sendMessage(
|
|
241
|
+
prompt: string,
|
|
242
|
+
model: string,
|
|
243
|
+
): Promise<void> {
|
|
244
|
+
const cookies = requireCookies();
|
|
245
|
+
|
|
246
|
+
const sessionId = getConvexSessionId();
|
|
247
|
+
if (!sessionId) {
|
|
248
|
+
throw new Error(
|
|
249
|
+
"Could not find convex-session-id in your cookies. Run `chat auth login` again.",
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const body = buildRequestBody(prompt, model, sessionId);
|
|
254
|
+
|
|
255
|
+
const { body: responseBody, httpStatus } = await curlRequest([
|
|
256
|
+
"-X",
|
|
257
|
+
"POST",
|
|
258
|
+
CHAT_API_URL,
|
|
259
|
+
"-H",
|
|
260
|
+
"Content-Type: application/json",
|
|
261
|
+
...browserHeaders(cookies),
|
|
262
|
+
"-d",
|
|
263
|
+
JSON.stringify(body),
|
|
264
|
+
]);
|
|
265
|
+
|
|
266
|
+
if (httpStatus !== 200) {
|
|
267
|
+
if (httpStatus === 403 || responseBody.includes("Vercel Security")) {
|
|
268
|
+
throw new Error(
|
|
269
|
+
[
|
|
270
|
+
"Blocked by Vercel's bot protection (403).",
|
|
271
|
+
"Your cookies may have expired. Try:",
|
|
272
|
+
" 1. Open t3.chat in Chrome and send a message",
|
|
273
|
+
" 2. Run `chat auth login` again with fresh cookies",
|
|
274
|
+
"",
|
|
275
|
+
"Make sure to copy the FULL cookie header from a recent request.",
|
|
276
|
+
].join("\n"),
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
throw new Error(`Chat API returned HTTP ${httpStatus}: ${responseBody}`);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Parse SSE lines from the response body
|
|
283
|
+
const lines = responseBody.split("\n");
|
|
284
|
+
let hasOutput = false;
|
|
285
|
+
|
|
286
|
+
for (const line of lines) {
|
|
287
|
+
if (!line.startsWith("data: ")) continue;
|
|
288
|
+
const data = line.slice(6).trim();
|
|
289
|
+
if (data === "[DONE]") continue;
|
|
290
|
+
|
|
291
|
+
try {
|
|
292
|
+
const event = JSON.parse(data) as {
|
|
293
|
+
type: string;
|
|
294
|
+
delta?: string;
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
if (event.type === "text-delta" && event.delta) {
|
|
298
|
+
process.stdout.write(event.delta);
|
|
299
|
+
hasOutput = true;
|
|
300
|
+
}
|
|
301
|
+
} catch {
|
|
302
|
+
// Skip non-JSON lines
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (hasOutput) {
|
|
307
|
+
process.stdout.write("\n");
|
|
308
|
+
}
|
|
309
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"lib": ["ESNext"],
|
|
4
|
+
"target": "ESNext",
|
|
5
|
+
"module": "Preserve",
|
|
6
|
+
"moduleDetection": "force",
|
|
7
|
+
"moduleResolution": "bundler",
|
|
8
|
+
"allowImportingTsExtensions": true,
|
|
9
|
+
"verbatimModuleSyntax": true,
|
|
10
|
+
"noEmit": true,
|
|
11
|
+
"strict": true,
|
|
12
|
+
"skipLibCheck": true,
|
|
13
|
+
"noFallthroughCasesInSwitch": true,
|
|
14
|
+
"noUncheckedIndexedAccess": true
|
|
15
|
+
},
|
|
16
|
+
"include": ["src"]
|
|
17
|
+
}
|