@thomas-huang/caosi 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,110 @@
1
+ # caosi
2
+
3
+ [中文](README.zh-CN.md)
4
+
5
+ A local converter between LLM client protocols and named upstream providers. Point Claude Code, Codex, or Gemini CLI at `http://127.0.0.1:9999/{provider_name}`. Same protocol is passed through; OpenAI Chat, OpenAI Responses, Claude Messages, and Gemini are converted when they differ.
6
+
7
+ caosi is a converter, not a gateway.
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ npm install -g @thomas-huang/caosi
13
+ caosi --version
14
+ ```
15
+
16
+ Needs Node.js 18+ on macOS, Linux, or Windows (amd64). The npm package downloads a native binary from GitHub Releases.
17
+
18
+ ## Run
19
+
20
+ The first start writes a sample Provider File and exits. That is intentional: caosi will not listen on an empty config.
21
+
22
+ ```bash
23
+ caosi
24
+ ```
25
+
26
+ Edit `~/.caosi/providers.jsonc`. Set `api_key` and `base_url`. If Claude Code will talk to an OpenAI-compatible upstream, keep `model` (for example `deepseek-chat`) so the upstream does not see `claude-*`.
27
+
28
+ Start again:
29
+
30
+ ```bash
31
+ caosi
32
+ curl -s http://127.0.0.1:9999/health
33
+ ```
34
+
35
+ ## Point Claude Code at it
36
+
37
+ ```bash
38
+ export ANTHROPIC_BASE_URL=http://127.0.0.1:9999/deepseek
39
+ export ANTHROPIC_API_KEY=dummy
40
+ claude
41
+ ```
42
+
43
+ Replace `deepseek` with your Provider Name. caosi ignores the client key and uses the Provider's `api_key`. Claude Code sends `/v1/messages`; caosi converts when the upstream is not Claude.
44
+
45
+ ## Codex / OpenAI and Gemini CLI
46
+
47
+ OpenAI SDK, Chat Completions, and Codex:
48
+
49
+ ```bash
50
+ export OPENAI_BASE_URL=http://127.0.0.1:9999/deepseek/v1
51
+ export OPENAI_API_KEY=dummy
52
+ ```
53
+
54
+ Gemini CLI (`~/.gemini/.env` or the environment):
55
+
56
+ ```bash
57
+ export GEMINI_API_BASE=http://127.0.0.1:9999/deepseek
58
+ ```
59
+
60
+ ## Provider File
61
+
62
+ `~/.caosi/providers.jsonc` is JSONC keyed by Provider Name (the first URL path segment). `health` is reserved.
63
+
64
+ ```jsonc
65
+ {
66
+ "deepseek": {
67
+ "base_url": "https://api.deepseek.com",
68
+ "protocol": "openai_chat",
69
+ "api_key": "sk-...",
70
+ "model": "deepseek-chat"
71
+ }
72
+ }
73
+ ```
74
+
75
+ | `protocol` | Upstream wire protocol |
76
+ |---|---|
77
+ | `openai_chat` | OpenAI Chat Completions |
78
+ | `openai_responses` | OpenAI Responses |
79
+ | `claude_messages` | Claude Messages |
80
+ | `gemini` | Gemini generateContent |
81
+
82
+ - `model` is optional: when set, it replaces the client's model name on the upstream request.
83
+ - `headers` is optional extra request headers (for example OpenRouter).
84
+ - `base_url` is a prefix. caosi does not strip `/v1`. Use the root the upstream actually expects (`https://api.deepseek.com`, not `https://api.deepseek.com/v1`).
85
+
86
+ ## Listen, health, reload
87
+
88
+ - Loopback only: `127.0.0.1` (or `::1` via `--listen`). Default port `9999` (`--port`).
89
+ - `GET /health`
90
+ - Saving `providers.jsonc` hot-reloads; a bad file keeps the last good config.
91
+ - Flags: `--config-dir`, `--port`, `--listen`, `--log-level`, `--version`.
92
+
93
+ ## What it does not do
94
+
95
+ No Web UI, OAuth, failover, key pools, or rewriting your Claude Code / Codex / Gemini config files.
96
+
97
+ ## Without Node.js
98
+
99
+ Download the binary for your OS from [GitHub Releases](https://github.com/thomas-huang/caosi/releases), rename it to `caosi` (or `caosi.exe` on Windows), and put it on your `PATH`. Checksums are in `checksums.txt` on each release.
100
+
101
+ ## Contributors
102
+
103
+ ```bash
104
+ go install github.com/thomas-huang/caosi/cmd/caosi@latest
105
+ go test ./...
106
+ ```
107
+
108
+ Domain language: [CONTEXT.md](CONTEXT.md). Decisions: [docs/adr](docs/adr).
109
+
110
+ To cut a release, push a tag `vX.Y.Z` (first public: `v0.1.0`). GitHub Actions builds the five binaries, writes checksums, creates the Release, and publishes `@thomas-huang/caosi` to npm with GitHub OIDC trusted publishing (no access token). On npmjs.com, add a GitHub Actions trusted publisher for package `@thomas-huang/caosi`: repository `thomas-huang/caosi`, workflow filename `release.yml`.
package/bin/caosi.js ADDED
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require("node:fs");
4
+ const path = require("node:path");
5
+ const { spawn } = require("node:child_process");
6
+
7
+ const binaryName = process.platform === "win32" ? "caosi.exe" : "caosi";
8
+ const binaryPath = path.join(__dirname, "..", "vendor", binaryName);
9
+
10
+ if (!fs.existsSync(binaryPath)) {
11
+ console.error(
12
+ "caosi: bundled binary is missing. Reinstall with `npm install -g @thomas-huang/caosi` after a GitHub Release exists, or run `npm rebuild @thomas-huang/caosi`."
13
+ );
14
+ process.exit(1);
15
+ }
16
+
17
+ const child = spawn(binaryPath, process.argv.slice(2), {
18
+ stdio: "inherit"
19
+ });
20
+
21
+ child.on("exit", (code, signal) => {
22
+ if (signal) {
23
+ process.kill(process.pid, signal);
24
+ return;
25
+ }
26
+ process.exit(code ?? 1);
27
+ });
28
+
29
+ child.on("error", (err) => {
30
+ console.error(`caosi: failed to start bundled binary: ${err.message}`);
31
+ process.exit(1);
32
+ });
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@thomas-huang/caosi",
3
+ "version": "0.1.0",
4
+ "description": "Local converter between LLM client protocols and named upstream providers",
5
+ "license": "MIT",
6
+ "homepage": "https://github.com/thomas-huang/caosi",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/thomas-huang/caosi.git"
10
+ },
11
+ "bugs": {
12
+ "url": "https://github.com/thomas-huang/caosi/issues"
13
+ },
14
+ "bin": {
15
+ "caosi": "bin/caosi.js"
16
+ },
17
+ "files": [
18
+ "bin/caosi.js",
19
+ "scripts/postinstall.js",
20
+ "README.md"
21
+ ],
22
+ "scripts": {
23
+ "postinstall": "node ./scripts/postinstall.js"
24
+ },
25
+ "keywords": [
26
+ "caosi",
27
+ "llm",
28
+ "converter",
29
+ "openai",
30
+ "claude",
31
+ "gemini",
32
+ "anthropic"
33
+ ],
34
+ "publishConfig": {
35
+ "access": "public",
36
+ "registry": "https://registry.npmjs.org/"
37
+ },
38
+ "engines": {
39
+ "node": ">=18"
40
+ }
41
+ }
@@ -0,0 +1,126 @@
1
+ #!/usr/bin/env node
2
+
3
+ const crypto = require("node:crypto");
4
+ const fs = require("node:fs");
5
+ const os = require("node:os");
6
+ const path = require("node:path");
7
+
8
+ const packageRoot = path.join(__dirname, "..");
9
+ const pkg = JSON.parse(fs.readFileSync(path.join(packageRoot, "package.json"), "utf8"));
10
+ const vendorDir = path.join(packageRoot, "vendor");
11
+
12
+ const repoBaseUrl =
13
+ process.env.CAOSI_NPM_BASE_URL || "https://github.com/thomas-huang/caosi/releases/download";
14
+ const version = String(pkg.version || "").trim();
15
+ const versionTag = version.startsWith("v") ? version : `v${version}`;
16
+
17
+ function resolveAssetName() {
18
+ switch (process.platform) {
19
+ case "darwin":
20
+ if (process.arch === "arm64") return "caosi-darwin-arm64";
21
+ if (process.arch === "x64") return "caosi-darwin-amd64";
22
+ break;
23
+ case "linux":
24
+ if (process.arch === "arm64") return "caosi-linux-arm64";
25
+ if (process.arch === "x64") return "caosi-linux-amd64";
26
+ break;
27
+ case "win32":
28
+ if (process.arch === "x64") return "caosi-windows-amd64.exe";
29
+ break;
30
+ default:
31
+ break;
32
+ }
33
+ throw new Error(`unsupported platform ${process.platform}/${process.arch}`);
34
+ }
35
+
36
+ function skipDownload() {
37
+ if (process.env.CAOSI_SKIP_DOWNLOAD === "1") {
38
+ return "CAOSI_SKIP_DOWNLOAD=1";
39
+ }
40
+ if (version === "" || version === "0.0.0-dev" || version === "dev") {
41
+ return `package version ${version || "(empty)"} has no GitHub Release`;
42
+ }
43
+ return "";
44
+ }
45
+
46
+ async function download(url, destination) {
47
+ const response = await fetch(url, {
48
+ headers: {
49
+ "user-agent": `caosi-npm/${version}`
50
+ },
51
+ redirect: "follow"
52
+ });
53
+ if (!response.ok) {
54
+ throw new Error(`download failed for ${url}: HTTP ${response.status}`);
55
+ }
56
+ const buf = Buffer.from(await response.arrayBuffer());
57
+ fs.writeFileSync(destination, buf);
58
+ }
59
+
60
+ function parseChecksums(text) {
61
+ const map = new Map();
62
+ for (const line of text.split(/\r?\n/)) {
63
+ const trimmed = line.trim();
64
+ if (!trimmed) continue;
65
+ const match = trimmed.match(/^([a-f0-9]{64})\s+\*?(.+)$/i);
66
+ if (!match) {
67
+ throw new Error(`invalid checksums line: ${line}`);
68
+ }
69
+ map.set(match[2], match[1].toLowerCase());
70
+ }
71
+ return map;
72
+ }
73
+
74
+ function sha256(filePath) {
75
+ const hash = crypto.createHash("sha256");
76
+ hash.update(fs.readFileSync(filePath));
77
+ return hash.digest("hex");
78
+ }
79
+
80
+ async function main() {
81
+ const reason = skipDownload();
82
+ if (reason) {
83
+ console.log(`caosi: skipping binary download (${reason})`);
84
+ return;
85
+ }
86
+
87
+ const assetName = resolveAssetName();
88
+ const binaryName = process.platform === "win32" ? "caosi.exe" : "caosi";
89
+ const targetPath = path.join(vendorDir, binaryName);
90
+ const checksumsUrl = `${repoBaseUrl}/${versionTag}/checksums.txt`;
91
+
92
+ fs.mkdirSync(vendorDir, { recursive: true });
93
+
94
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "caosi-npm-"));
95
+ const checksumsPath = path.join(tempDir, "checksums.txt");
96
+ const downloadPath = path.join(tempDir, assetName);
97
+
98
+ try {
99
+ console.log(`caosi: downloading ${assetName} for ${process.platform}/${process.arch}`);
100
+ await download(checksumsUrl, checksumsPath);
101
+ const checksums = parseChecksums(fs.readFileSync(checksumsPath, "utf8"));
102
+ const expectedSha = checksums.get(assetName);
103
+ if (!expectedSha) {
104
+ throw new Error(`checksums.txt does not contain ${assetName}`);
105
+ }
106
+
107
+ await download(`${repoBaseUrl}/${versionTag}/${assetName}`, downloadPath);
108
+ const actualSha = sha256(downloadPath);
109
+ if (actualSha !== expectedSha) {
110
+ throw new Error(`checksum mismatch for ${assetName}`);
111
+ }
112
+
113
+ fs.copyFileSync(downloadPath, targetPath);
114
+ if (process.platform !== "win32") {
115
+ fs.chmodSync(targetPath, 0o755);
116
+ }
117
+ console.log(`caosi: installed binary to ${path.relative(packageRoot, targetPath)}`);
118
+ } finally {
119
+ fs.rmSync(tempDir, { recursive: true, force: true });
120
+ }
121
+ }
122
+
123
+ main().catch((err) => {
124
+ console.error(`caosi: postinstall failed: ${err.message}`);
125
+ process.exit(1);
126
+ });