@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/dist/plugin.js CHANGED
@@ -1,42 +1,6 @@
1
- import { createAuditLogger, createOperatorTools, createProfileTools, switchToNextProfile, generateCorrelationId, loadProfiles, discoverTargetsFromProfiles, discoverTargets, createAuthWatcher, validateConfig, createRegistry, createRoutingEventBus, createCircuitBreaker, createConcurrencyTracker, createCooldownManager, createRequestTraceBuffer, createLogSubscriber } from './chunk-XXH633FY.js';
2
- import { mkdir, writeFile, rename } from 'fs/promises';
3
- import { join, dirname } from 'path';
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.1",
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",