@thomas-huang/caosi 0.1.3 → 0.2.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 +25 -13
- package/bin/caosi.js +29 -3
- package/package.json +2 -5
- package/vendor/caosi-darwin-amd64 +0 -0
- package/vendor/caosi-darwin-arm64 +0 -0
- package/vendor/caosi-linux-amd64 +0 -0
- package/vendor/caosi-linux-arm64 +0 -0
- package/vendor/caosi-windows-amd64.exe +0 -0
- package/scripts/postinstall.js +0 -126
package/README.md
CHANGED
|
@@ -2,9 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
[中文](README.zh-CN.md)
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Keep using Claude Code, Codex, or Gemini CLI. Point them at your own upstream — no client switch.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Set the Base URL to `http://127.0.0.1:9999/{provider_name}`. Same protocol is passed through; caosi converts when they differ.
|
|
8
|
+
|
|
9
|
+
- A single static binary
|
|
10
|
+
- npm ships the binaries: no install script, nothing fetched from GitHub
|
|
11
|
+
- macOS, Linux, and Windows
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install -g @thomas-huang/caosi
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
No Web UI, no OAuth, no key pool, no client-config rewrite. One process, one file, loopback only.
|
|
8
18
|
|
|
9
19
|
## Install
|
|
10
20
|
|
|
@@ -13,7 +23,7 @@ npm install -g @thomas-huang/caosi
|
|
|
13
23
|
caosi --version
|
|
14
24
|
```
|
|
15
25
|
|
|
16
|
-
Needs Node.js 18+ on macOS, Linux, or Windows (amd64). The npm package
|
|
26
|
+
Needs Node.js 18+ on macOS, Linux, or Windows (amd64). The npm package includes native binaries; there is no install script and nothing is fetched from GitHub at install time.
|
|
17
27
|
|
|
18
28
|
## Run
|
|
19
29
|
|
|
@@ -23,7 +33,7 @@ The first start writes a sample Provider File and exits. That is intentional: ca
|
|
|
23
33
|
caosi
|
|
24
34
|
```
|
|
25
35
|
|
|
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 `
|
|
36
|
+
Edit `~/.caosi/providers.jsonc`. Set `api_key` and `base_url`. If Claude Code will talk to an OpenAI-compatible upstream, keep `model` (for example `qwen/qwen3-coder`) so the upstream does not see `claude-*`.
|
|
27
37
|
|
|
28
38
|
Start again:
|
|
29
39
|
|
|
@@ -35,26 +45,26 @@ curl -s http://127.0.0.1:9999/health
|
|
|
35
45
|
## Point Claude Code at it
|
|
36
46
|
|
|
37
47
|
```bash
|
|
38
|
-
export ANTHROPIC_BASE_URL=http://127.0.0.1:9999/
|
|
48
|
+
export ANTHROPIC_BASE_URL=http://127.0.0.1:9999/openrouter
|
|
39
49
|
export ANTHROPIC_API_KEY=dummy
|
|
40
50
|
claude
|
|
41
51
|
```
|
|
42
52
|
|
|
43
|
-
Replace `
|
|
53
|
+
Replace `openrouter` 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
54
|
|
|
45
55
|
## Codex / OpenAI and Gemini CLI
|
|
46
56
|
|
|
47
57
|
OpenAI SDK, Chat Completions, and Codex:
|
|
48
58
|
|
|
49
59
|
```bash
|
|
50
|
-
export OPENAI_BASE_URL=http://127.0.0.1:9999/
|
|
60
|
+
export OPENAI_BASE_URL=http://127.0.0.1:9999/openrouter/v1
|
|
51
61
|
export OPENAI_API_KEY=dummy
|
|
52
62
|
```
|
|
53
63
|
|
|
54
64
|
Gemini CLI (`~/.gemini/.env` or the environment):
|
|
55
65
|
|
|
56
66
|
```bash
|
|
57
|
-
export GEMINI_API_BASE=http://127.0.0.1:9999/
|
|
67
|
+
export GEMINI_API_BASE=http://127.0.0.1:9999/openrouter
|
|
58
68
|
```
|
|
59
69
|
|
|
60
70
|
## Provider File
|
|
@@ -63,11 +73,11 @@ export GEMINI_API_BASE=http://127.0.0.1:9999/deepseek
|
|
|
63
73
|
|
|
64
74
|
```jsonc
|
|
65
75
|
{
|
|
66
|
-
"
|
|
67
|
-
"base_url": "https://api
|
|
76
|
+
"openrouter": {
|
|
77
|
+
"base_url": "https://openrouter.ai/api",
|
|
68
78
|
"protocol": "openai_chat",
|
|
69
79
|
"api_key": "sk-...",
|
|
70
|
-
"model": "
|
|
80
|
+
"model": "qwen/qwen3-coder"
|
|
71
81
|
}
|
|
72
82
|
}
|
|
73
83
|
```
|
|
@@ -81,7 +91,7 @@ export GEMINI_API_BASE=http://127.0.0.1:9999/deepseek
|
|
|
81
91
|
|
|
82
92
|
- `model` is optional: when set, it replaces the client's model name on the upstream request.
|
|
83
93
|
- `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
|
|
94
|
+
- `base_url` is a prefix. caosi does not strip `/v1`. Use the root the upstream actually expects (`https://openrouter.ai/api`, not `https://openrouter.ai/api/v1`).
|
|
85
95
|
|
|
86
96
|
## Listen, health, reload
|
|
87
97
|
|
|
@@ -92,6 +102,8 @@ export GEMINI_API_BASE=http://127.0.0.1:9999/deepseek
|
|
|
92
102
|
|
|
93
103
|
## What it does not do
|
|
94
104
|
|
|
105
|
+
caosi is a converter, not a gateway.
|
|
106
|
+
|
|
95
107
|
No Web UI, OAuth, failover, key pools, or rewriting your Claude Code / Codex / Gemini config files.
|
|
96
108
|
|
|
97
109
|
## Without Node.js
|
|
@@ -105,7 +117,7 @@ go install github.com/thomas-huang/caosi/cmd/caosi@latest
|
|
|
105
117
|
go test ./...
|
|
106
118
|
```
|
|
107
119
|
|
|
108
|
-
Domain language: [CONTEXT.md](CONTEXT.md). Decisions: [docs/adr](docs/adr).
|
|
120
|
+
Domain language: [CONTEXT.md](CONTEXT.md). Architecture: [docs/architecture.md](docs/architecture.md). Decisions: [docs/adr](docs/adr).
|
|
109
121
|
|
|
110
122
|
To cut a release, push a tag `vX.Y.Z`. 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).
|
|
111
123
|
|
package/bin/caosi.js
CHANGED
|
@@ -4,12 +4,38 @@ const fs = require("node:fs");
|
|
|
4
4
|
const path = require("node:path");
|
|
5
5
|
const { spawn } = require("node:child_process");
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
function resolveAssetName() {
|
|
8
|
+
switch (process.platform) {
|
|
9
|
+
case "darwin":
|
|
10
|
+
if (process.arch === "arm64") return "caosi-darwin-arm64";
|
|
11
|
+
if (process.arch === "x64") return "caosi-darwin-amd64";
|
|
12
|
+
break;
|
|
13
|
+
case "linux":
|
|
14
|
+
if (process.arch === "arm64") return "caosi-linux-arm64";
|
|
15
|
+
if (process.arch === "x64") return "caosi-linux-amd64";
|
|
16
|
+
break;
|
|
17
|
+
case "win32":
|
|
18
|
+
if (process.arch === "x64") return "caosi-windows-amd64.exe";
|
|
19
|
+
break;
|
|
20
|
+
default:
|
|
21
|
+
break;
|
|
22
|
+
}
|
|
23
|
+
return "";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const assetName = resolveAssetName();
|
|
27
|
+
const binaryPath = assetName
|
|
28
|
+
? path.join(__dirname, "..", "vendor", assetName)
|
|
29
|
+
: "";
|
|
30
|
+
|
|
31
|
+
if (!assetName) {
|
|
32
|
+
console.error(`caosi: unsupported platform ${process.platform}/${process.arch}`);
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
9
35
|
|
|
10
36
|
if (!fs.existsSync(binaryPath)) {
|
|
11
37
|
console.error(
|
|
12
|
-
"caosi: bundled binary is missing. Reinstall with `npm install -g @thomas-huang/caosi
|
|
38
|
+
"caosi: bundled binary is missing. Reinstall with `npm install -g @thomas-huang/caosi`."
|
|
13
39
|
);
|
|
14
40
|
process.exit(1);
|
|
15
41
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@thomas-huang/caosi",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Local converter between LLM client protocols and named upstream providers",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://github.com/thomas-huang/caosi",
|
|
@@ -16,12 +16,9 @@
|
|
|
16
16
|
},
|
|
17
17
|
"files": [
|
|
18
18
|
"bin/caosi.js",
|
|
19
|
-
"
|
|
19
|
+
"vendor/",
|
|
20
20
|
"README.md"
|
|
21
21
|
],
|
|
22
|
-
"scripts": {
|
|
23
|
-
"postinstall": "node ./scripts/postinstall.js"
|
|
24
|
-
},
|
|
25
22
|
"keywords": [
|
|
26
23
|
"caosi",
|
|
27
24
|
"llm",
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/scripts/postinstall.js
DELETED
|
@@ -1,126 +0,0 @@
|
|
|
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
|
-
});
|