@albinocrabs/o-switcher 0.3.1 → 0.4.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/CHANGELOG.md +11 -0
- package/dist/{chunk-XXH633FY.js → chunk-2EYMYNKS.js} +3 -101
- package/dist/chunk-BHHR2U5B.cjs +123 -0
- package/dist/chunk-HAEEX3KB.js +106 -0
- package/dist/chunk-I6RDAZBR.js +231 -0
- package/dist/chunk-QKEHCM2F.cjs +234 -0
- package/dist/{chunk-H72U2MNG.cjs → chunk-TGYAV5TV.cjs} +18 -128
- package/dist/index.cjs +104 -103
- package/dist/index.js +3 -2
- package/dist/plugin.cjs +44 -58
- package/dist/plugin.js +25 -39
- package/dist/proxy/cli.cjs +91 -0
- package/dist/proxy/cli.d.cts +1 -0
- package/dist/proxy/cli.d.ts +1 -0
- package/dist/proxy/cli.js +89 -0
- package/package.json +4 -1
package/dist/plugin.js
CHANGED
|
@@ -1,42 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { homedir } from 'os';
|
|
5
|
-
|
|
6
|
-
var STATE_DIR = join(homedir(), ".local", "share", "o-switcher");
|
|
7
|
-
var STATE_FILE = join(STATE_DIR, "tui-state.json");
|
|
8
|
-
var STATE_TMP = join(STATE_DIR, "tui-state.json.tmp");
|
|
9
|
-
var writeStateAtomic = async (state) => {
|
|
10
|
-
await mkdir(dirname(STATE_FILE), { recursive: true });
|
|
11
|
-
const json = JSON.stringify(state);
|
|
12
|
-
await writeFile(STATE_TMP, json, "utf8");
|
|
13
|
-
await rename(STATE_TMP, STATE_FILE);
|
|
14
|
-
};
|
|
15
|
-
var createStateWriter = (debounceMs = 500) => {
|
|
16
|
-
let pending;
|
|
17
|
-
let timer;
|
|
18
|
-
let writePromise = Promise.resolve();
|
|
19
|
-
const doWrite = () => {
|
|
20
|
-
if (!pending) return;
|
|
21
|
-
const snapshot = pending;
|
|
22
|
-
pending = void 0;
|
|
23
|
-
writePromise = writeStateAtomic(snapshot).catch(() => void 0);
|
|
24
|
-
};
|
|
25
|
-
const write = (state) => {
|
|
26
|
-
pending = state;
|
|
27
|
-
if (timer) clearTimeout(timer);
|
|
28
|
-
timer = setTimeout(doWrite, debounceMs);
|
|
29
|
-
};
|
|
30
|
-
const flush = async () => {
|
|
31
|
-
if (timer) {
|
|
32
|
-
clearTimeout(timer);
|
|
33
|
-
timer = void 0;
|
|
34
|
-
}
|
|
35
|
-
doWrite();
|
|
36
|
-
await writePromise;
|
|
37
|
-
};
|
|
38
|
-
return { write, flush };
|
|
39
|
-
};
|
|
1
|
+
import { createOperatorTools, createProfileTools, switchToNextProfile, discoverTargetsFromProfiles, discoverTargets, createAuthWatcher, validateConfig, createRegistry, createRoutingEventBus, createCircuitBreaker, createConcurrencyTracker, createCooldownManager, createRequestTraceBuffer, createLogSubscriber } from './chunk-2EYMYNKS.js';
|
|
2
|
+
import { createStateWriter, createProxy } from './chunk-I6RDAZBR.js';
|
|
3
|
+
import { createAuditLogger, generateCorrelationId, loadProfiles } from './chunk-HAEEX3KB.js';
|
|
40
4
|
|
|
41
5
|
// src/plugin.ts
|
|
42
6
|
var initializeSwitcher = (rawConfig) => {
|
|
@@ -163,6 +127,28 @@ var server = async (_input) => {
|
|
|
163
127
|
Object.assign(state, initialized);
|
|
164
128
|
logger.info({ targets: state.registry?.getAllTargets().length }, "O-Switcher initialized");
|
|
165
129
|
publishTuiState();
|
|
130
|
+
const proxyEnabled = switcherConfig?.["proxy"] !== false && !process.env["VITEST"];
|
|
131
|
+
if (proxyEnabled) {
|
|
132
|
+
const proxyPort = Number(switcherConfig?.["proxy_port"] ?? 4444);
|
|
133
|
+
try {
|
|
134
|
+
const proxy = await createProxy({
|
|
135
|
+
port: proxyPort,
|
|
136
|
+
provider: "openai",
|
|
137
|
+
maxRetries: 3
|
|
138
|
+
});
|
|
139
|
+
await proxy.start();
|
|
140
|
+
logger.info({ port: proxyPort }, "Proxy started \u2014 rotating API keys on 429");
|
|
141
|
+
if (providerConfig) {
|
|
142
|
+
const providerEntry = providerConfig["openai"];
|
|
143
|
+
if (providerEntry && typeof providerEntry === "object") {
|
|
144
|
+
providerEntry["baseURL"] = `http://localhost:${proxyPort}/v1`;
|
|
145
|
+
logger.info("Patched openai provider baseURL to proxy");
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
} catch (proxyErr) {
|
|
149
|
+
logger.warn({ err: proxyErr }, "Proxy failed to start \u2014 continuing without key rotation");
|
|
150
|
+
}
|
|
151
|
+
}
|
|
166
152
|
state.authWatcher = createAuthWatcher({ logger });
|
|
167
153
|
await state.authWatcher.start();
|
|
168
154
|
logger.info("Auth watcher started");
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
var chunkQKEHCM2F_cjs = require('../chunk-QKEHCM2F.cjs');
|
|
5
|
+
require('../chunk-BHHR2U5B.cjs');
|
|
6
|
+
var promises = require('fs/promises');
|
|
7
|
+
var path = require('path');
|
|
8
|
+
var os = require('os');
|
|
9
|
+
|
|
10
|
+
var OPENCODE_CONFIG = path.join(os.homedir(), ".config", "opencode", "opencode.json");
|
|
11
|
+
var OPENCODE_BACKUP = `${OPENCODE_CONFIG}.osw-backup`;
|
|
12
|
+
var parseArgs = (args) => {
|
|
13
|
+
const result = {};
|
|
14
|
+
for (let i = 0; i < args.length; i++) {
|
|
15
|
+
const arg = args[i];
|
|
16
|
+
if (arg.startsWith("--") && i + 1 < args.length) {
|
|
17
|
+
result[arg.slice(2)] = args[i + 1];
|
|
18
|
+
i++;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return result;
|
|
22
|
+
};
|
|
23
|
+
var patchConfig = async (provider, port, _upstream) => {
|
|
24
|
+
try {
|
|
25
|
+
const raw = await promises.readFile(OPENCODE_CONFIG, "utf-8");
|
|
26
|
+
const config = JSON.parse(raw);
|
|
27
|
+
await promises.copyFile(OPENCODE_CONFIG, OPENCODE_BACKUP);
|
|
28
|
+
const providers = config["provider"] ?? {};
|
|
29
|
+
const existing = providers[provider] ?? {};
|
|
30
|
+
const originalBaseURL = existing["baseURL"];
|
|
31
|
+
existing["baseURL"] = `http://localhost:${port}/v1`;
|
|
32
|
+
providers[provider] = existing;
|
|
33
|
+
config["provider"] = providers;
|
|
34
|
+
await promises.writeFile(OPENCODE_CONFIG, JSON.stringify(config, null, 2), "utf-8");
|
|
35
|
+
return originalBaseURL ?? null;
|
|
36
|
+
} catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
var restoreConfig = async () => {
|
|
41
|
+
try {
|
|
42
|
+
await promises.copyFile(OPENCODE_BACKUP, OPENCODE_CONFIG);
|
|
43
|
+
console.log(" Config restored.");
|
|
44
|
+
} catch {
|
|
45
|
+
console.log(" Warning: could not restore config backup.");
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
var main = async () => {
|
|
49
|
+
const args = parseArgs(process.argv.slice(2));
|
|
50
|
+
const config = {
|
|
51
|
+
port: args["port"] ? Number(args["port"]) : 4444,
|
|
52
|
+
upstream: args["upstream"] ?? "https://api.openai.com",
|
|
53
|
+
provider: args["provider"] ?? "openai",
|
|
54
|
+
maxRetries: args["retries"] ? Number(args["retries"]) : 3
|
|
55
|
+
};
|
|
56
|
+
await patchConfig(config.provider, config.port, config.upstream);
|
|
57
|
+
console.log(`
|
|
58
|
+
\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510
|
|
59
|
+
\u2502 O-Switcher Proxy \u2502
|
|
60
|
+
\u2502 \u2502
|
|
61
|
+
\u2502 Port: ${String(config.port).padEnd(28)}\u2502
|
|
62
|
+
\u2502 Upstream: ${config.upstream.padEnd(28)}\u2502
|
|
63
|
+
\u2502 Provider: ${config.provider.padEnd(28)}\u2502
|
|
64
|
+
\u2502 Retries: ${String(config.maxRetries).padEnd(28)}\u2502
|
|
65
|
+
\u2502 \u2502
|
|
66
|
+
\u2502 opencode.json patched automatically \u2502
|
|
67
|
+
\u2502 Will restore on exit (Ctrl+C) \u2502
|
|
68
|
+
\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518
|
|
69
|
+
`);
|
|
70
|
+
const proxy = await chunkQKEHCM2F_cjs.createProxy(config);
|
|
71
|
+
const { port, close } = await proxy.start();
|
|
72
|
+
console.log(` Listening on http://localhost:${port}`);
|
|
73
|
+
console.log(" Restart OpenCode to pick up the proxy.");
|
|
74
|
+
console.log(" Press Ctrl+C to stop and restore config.");
|
|
75
|
+
const shutdown = async () => {
|
|
76
|
+
console.log("\n Shutting down...");
|
|
77
|
+
close();
|
|
78
|
+
await restoreConfig();
|
|
79
|
+
process.exit(0);
|
|
80
|
+
};
|
|
81
|
+
process.on("SIGINT", () => {
|
|
82
|
+
shutdown();
|
|
83
|
+
});
|
|
84
|
+
process.on("SIGTERM", () => {
|
|
85
|
+
shutdown();
|
|
86
|
+
});
|
|
87
|
+
};
|
|
88
|
+
main().catch((err) => {
|
|
89
|
+
console.error("Failed to start proxy:", err);
|
|
90
|
+
process.exit(1);
|
|
91
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createProxy } from '../chunk-I6RDAZBR.js';
|
|
3
|
+
import '../chunk-HAEEX3KB.js';
|
|
4
|
+
import { readFile, copyFile, writeFile } from 'fs/promises';
|
|
5
|
+
import { join } from 'path';
|
|
6
|
+
import { homedir } from 'os';
|
|
7
|
+
|
|
8
|
+
var OPENCODE_CONFIG = join(homedir(), ".config", "opencode", "opencode.json");
|
|
9
|
+
var OPENCODE_BACKUP = `${OPENCODE_CONFIG}.osw-backup`;
|
|
10
|
+
var parseArgs = (args) => {
|
|
11
|
+
const result = {};
|
|
12
|
+
for (let i = 0; i < args.length; i++) {
|
|
13
|
+
const arg = args[i];
|
|
14
|
+
if (arg.startsWith("--") && i + 1 < args.length) {
|
|
15
|
+
result[arg.slice(2)] = args[i + 1];
|
|
16
|
+
i++;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return result;
|
|
20
|
+
};
|
|
21
|
+
var patchConfig = async (provider, port, _upstream) => {
|
|
22
|
+
try {
|
|
23
|
+
const raw = await readFile(OPENCODE_CONFIG, "utf-8");
|
|
24
|
+
const config = JSON.parse(raw);
|
|
25
|
+
await copyFile(OPENCODE_CONFIG, OPENCODE_BACKUP);
|
|
26
|
+
const providers = config["provider"] ?? {};
|
|
27
|
+
const existing = providers[provider] ?? {};
|
|
28
|
+
const originalBaseURL = existing["baseURL"];
|
|
29
|
+
existing["baseURL"] = `http://localhost:${port}/v1`;
|
|
30
|
+
providers[provider] = existing;
|
|
31
|
+
config["provider"] = providers;
|
|
32
|
+
await writeFile(OPENCODE_CONFIG, JSON.stringify(config, null, 2), "utf-8");
|
|
33
|
+
return originalBaseURL ?? null;
|
|
34
|
+
} catch {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
var restoreConfig = async () => {
|
|
39
|
+
try {
|
|
40
|
+
await copyFile(OPENCODE_BACKUP, OPENCODE_CONFIG);
|
|
41
|
+
console.log(" Config restored.");
|
|
42
|
+
} catch {
|
|
43
|
+
console.log(" Warning: could not restore config backup.");
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
var main = async () => {
|
|
47
|
+
const args = parseArgs(process.argv.slice(2));
|
|
48
|
+
const config = {
|
|
49
|
+
port: args["port"] ? Number(args["port"]) : 4444,
|
|
50
|
+
upstream: args["upstream"] ?? "https://api.openai.com",
|
|
51
|
+
provider: args["provider"] ?? "openai",
|
|
52
|
+
maxRetries: args["retries"] ? Number(args["retries"]) : 3
|
|
53
|
+
};
|
|
54
|
+
await patchConfig(config.provider, config.port, config.upstream);
|
|
55
|
+
console.log(`
|
|
56
|
+
\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510
|
|
57
|
+
\u2502 O-Switcher Proxy \u2502
|
|
58
|
+
\u2502 \u2502
|
|
59
|
+
\u2502 Port: ${String(config.port).padEnd(28)}\u2502
|
|
60
|
+
\u2502 Upstream: ${config.upstream.padEnd(28)}\u2502
|
|
61
|
+
\u2502 Provider: ${config.provider.padEnd(28)}\u2502
|
|
62
|
+
\u2502 Retries: ${String(config.maxRetries).padEnd(28)}\u2502
|
|
63
|
+
\u2502 \u2502
|
|
64
|
+
\u2502 opencode.json patched automatically \u2502
|
|
65
|
+
\u2502 Will restore on exit (Ctrl+C) \u2502
|
|
66
|
+
\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518
|
|
67
|
+
`);
|
|
68
|
+
const proxy = await createProxy(config);
|
|
69
|
+
const { port, close } = await proxy.start();
|
|
70
|
+
console.log(` Listening on http://localhost:${port}`);
|
|
71
|
+
console.log(" Restart OpenCode to pick up the proxy.");
|
|
72
|
+
console.log(" Press Ctrl+C to stop and restore config.");
|
|
73
|
+
const shutdown = async () => {
|
|
74
|
+
console.log("\n Shutting down...");
|
|
75
|
+
close();
|
|
76
|
+
await restoreConfig();
|
|
77
|
+
process.exit(0);
|
|
78
|
+
};
|
|
79
|
+
process.on("SIGINT", () => {
|
|
80
|
+
shutdown();
|
|
81
|
+
});
|
|
82
|
+
process.on("SIGTERM", () => {
|
|
83
|
+
shutdown();
|
|
84
|
+
});
|
|
85
|
+
};
|
|
86
|
+
main().catch((err) => {
|
|
87
|
+
console.error("Failed to start proxy:", err);
|
|
88
|
+
process.exit(1);
|
|
89
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@albinocrabs/o-switcher",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Seamless OpenRouter profile rotation for OpenCode — buy multiple subscriptions, use as one pool",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -27,6 +27,9 @@
|
|
|
27
27
|
"LICENSE",
|
|
28
28
|
"CHANGELOG.md"
|
|
29
29
|
],
|
|
30
|
+
"bin": {
|
|
31
|
+
"o-switcher-proxy": "dist/proxy/cli.js"
|
|
32
|
+
},
|
|
30
33
|
"main": "dist/plugin.js",
|
|
31
34
|
"module": "dist/plugin.js",
|
|
32
35
|
"types": "dist/plugin.d.ts",
|