Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -516,9 +516,6 @@ function injectModelsConfig(
}
}

function readStringSafe(value: unknown): string | undefined {
return typeof value === "string" && value.trim() ? value.trim() : undefined;
}

/**
* Inject dummy auth profile for BlockRun into agent auth stores.
Expand Down
89 changes: 89 additions & 0 deletions src/proxy.routing-config-reuse.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { describe, expect, it } from "vitest";
import { generatePrivateKey } from "viem/accounts";

import { startProxy } from "./proxy.js";
import { DEFAULT_ROUTING_CONFIG } from "./router/index.js";

describe("startProxy routing config reuse", () => {
it("applies custom routing config when reusing an existing proxy", async () => {
const walletKey = generatePrivateKey();
const port = 21000 + Math.floor(Math.random() * 10000);

// Start the first proxy (uses DEFAULT_ROUTING_CONFIG)
const firstProxy = await startProxy({
wallet: walletKey,
port,
skipBalanceCheck: true,
});

try {
// Verify initial config is the default
const initialRes = await fetch(`${firstProxy.baseUrl}/__routing-config`);
expect(initialRes.status).toBe(200);
const initialConfig = await initialRes.json();
expect(initialConfig.version).toBe(DEFAULT_ROUTING_CONFIG.version);

// Custom routing config with a modified tier
const customConfig: Parameters<typeof startProxy>[0]["routingConfig"] = {
tiers: {
SIMPLE: { primary: "test-model-simple", fallback: ["test-fallback-1"] },
} as Record<string, { primary: string; fallback: string[] }>,
};

// Start proxy again on same port — enters reuse path
const secondProxy = await startProxy({
wallet: walletKey,
port,
skipBalanceCheck: true,
routingConfig: customConfig,
});

// The second proxy's close is a no-op (reuse path)
await secondProxy.close();

// Verify the routing config was updated on the running proxy
const updatedRes = await fetch(`${firstProxy.baseUrl}/__routing-config`);
expect(updatedRes.status).toBe(200);
const updatedConfig = await updatedRes.json();
expect(updatedConfig.tiers.SIMPLE.primary).toBe("test-model-simple");
expect(updatedConfig.tiers.SIMPLE.fallback).toEqual(["test-fallback-1"]);

// Other tiers should still have defaults (merged, not replaced)
expect(updatedConfig.tiers.COMPLEX.primary).toBe(
DEFAULT_ROUTING_CONFIG.tiers.COMPLEX.primary,
);
} finally {
await firstProxy.close();
}
});

it("leaves default routing config when reusing without routingConfig option", async () => {
const walletKey = generatePrivateKey();
const port = 21000 + Math.floor(Math.random() * 10000);

const firstProxy = await startProxy({
wallet: walletKey,
port,
skipBalanceCheck: true,
});

try {
// Reuse without routingConfig
const secondProxy = await startProxy({
wallet: walletKey,
port,
skipBalanceCheck: true,
});
await secondProxy.close();

// Config should still be the default
const res = await fetch(`${firstProxy.baseUrl}/__routing-config`);
expect(res.status).toBe(200);
const config = await res.json();
expect(config.version).toBe(DEFAULT_ROUTING_CONFIG.version);
expect(config.tiers).toEqual(DEFAULT_ROUTING_CONFIG.tiers);
} finally {
await firstProxy.close();
}
});
});
56 changes: 56 additions & 0 deletions src/proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1621,6 +1621,26 @@ export async function startProxy(options: ProxyOptions): Promise<ProxyHandle> {
balanceMonitor = new BalanceMonitor(account.address);
}

// If a routing config was provided, push it to the running proxy so it takes effect
if (options.routingConfig) {
try {
const updateRes = await fetch(`${baseUrl}/__update-routing`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ routingConfig: options.routingConfig }),
});
if (!updateRes.ok) {
console.warn(
`[ClawRouter] Failed to update routing config on existing proxy: ${updateRes.status}`,
);
}
} catch (err) {
console.warn(
`[ClawRouter] Could not update routing config on existing proxy: ${err instanceof Error ? err.message : String(err)}`,
);
}
}

options.onReady?.(listenPort);

return {
Expand Down Expand Up @@ -1774,6 +1794,42 @@ export async function startProxy(options: ProxyOptions): Promise<ProxyHandle> {
return;
}

// Internal endpoint: update routing config on a running proxy (used by reuse path)
if (req.method === "POST" && req.url === "/__update-routing") {
try {
const chunks: Buffer[] = [];
for await (const chunk of req) {
chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
}
const body = JSON.parse(Buffer.concat(chunks).toString("utf-8")) as {
routingConfig?: Partial<RoutingConfig>;
};
if (body.routingConfig) {
routerOpts.config = mergeRoutingConfig(body.routingConfig);
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ updated: true }));
} else {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ updated: false }));
}
} catch (err) {
res.writeHead(400, { "Content-Type": "application/json" });
res.end(
JSON.stringify({
error: `Invalid routing config: ${err instanceof Error ? err.message : String(err)}`,
}),
);
}
return;
}

// Internal endpoint: read current routing config (for testing/debugging)
if (req.method === "GET" && req.url === "/__routing-config") {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify(routerOpts.config));
return;
}

// Cache stats endpoint
if (req.url === "/cache" || req.url?.startsWith("/cache?")) {
const stats = responseCache.getStats();
Expand Down
Loading