Skip to content
Merged
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: 3 additions & 0 deletions src/adapters/openai-chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,9 @@ export function buildOpenAIChatPassthroughRequest(
if (rawBody[field] !== undefined) body[field] = rawBody[field];
}

const openRouterRouting = resolveOpenRouterRouting(provider, modelId);
if (openRouterRouting) body.provider = openRouterProviderPayload(openRouterRouting);

if (modelInList(provider.noTemperatureModels, modelId)) delete body.temperature;
if (modelInList(provider.noTopPModels, modelId)) delete body.top_p;
if (modelInList(provider.noPenaltyModels, modelId)) {
Expand Down
104 changes: 103 additions & 1 deletion tests/openrouter-provider-routing.test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
import { describe, expect, test } from "bun:test";
import { createOpenAIChatAdapter } from "../src/adapters/openai-chat";
import { mkdtempSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { buildOpenAIChatPassthroughRequest, createOpenAIChatAdapter } from "../src/adapters/openai-chat";
import {
openRouterRoutingConfigError,
openRouterProviderPayload,
} from "../src/providers/openrouter-routing";
import { clearKeyCooldowns, rotateProviderTransportOn429 } from "../src/providers/key-failover";
import { routeModel } from "../src/router";
import { providerManagementConfigError, safeConfigDTO } from "../src/server/auth-cors";
import type { OcxConfig, OcxParsedRequest, OcxProviderConfig } from "../src/types";
import { removeTreeWithRetry } from "./helpers/remove-tree";

function provider(baseUrl: string, overrides: Partial<OcxProviderConfig> = {}): OcxProviderConfig {
return { adapter: "openai-chat", baseUrl, apiKey: "test-key", ...overrides };
Expand All @@ -26,6 +31,18 @@ function body(baseUrl: string, modelId: string, overrides: Partial<OcxProviderCo
return JSON.parse(request.body as string) as Record<string, unknown>;
}

function passthroughBody(
providerConfig: OcxProviderConfig,
modelId: string,
rawBody: Record<string, unknown> = {},
): Record<string, unknown> {
const request = buildOpenAIChatPassthroughRequest(providerConfig, {
messages: [{ role: "user", content: "hello" }],
...rawBody,
}, modelId, false);
return JSON.parse(request.body as string) as Record<string, unknown>;
}

describe("OpenRouter configurable provider routing", () => {
const deepSeekLock = { openRouterRouting: { only: ["deepseek"], allowFallbacks: false } };

Expand Down Expand Up @@ -100,6 +117,91 @@ describe("OpenRouter configurable provider routing", () => {
expect(requestBody.stream_options).toEqual({ include_usage: true });
});

test("preserves exact model routing on native Chat passthrough requests", () => {
const requestBody = passthroughBody(provider("https://openrouter.ai/api/v1", {
openRouterRouting: { order: ["deepseek"], allowFallbacks: true },
modelOpenRouterRouting: {
"anthropic/claude-sonnet-5": { only: ["anthropic"], allowFallbacks: false },
},
}), "anthropic/claude-sonnet-5", {
provider: { only: ["caller-controlled"] },
});

expect(requestBody.provider).toEqual({
only: ["anthropic"], allow_fallbacks: false,
});
});

test("preserves provider-wide routing on native Chat passthrough requests", () => {
expect(passthroughBody(
provider("https://openrouter.ai/api/v1", deepSeekLock),
"deepseek/deepseek-chat",
).provider).toEqual({ only: ["deepseek"], allow_fallbacks: false });
});

test("resolves routed aliases before applying native Chat model preferences", () => {
const nativeModelId = "anthropic/claude-sonnet-5";
const config: OcxConfig = {
port: 10100,
defaultProvider: "openrouter",
providers: {
openrouter: provider("https://openrouter.ai/api/v1", {
models: [nativeModelId],
openRouterRouting: { only: ["deepseek"] },
modelOpenRouterRouting: {
[nativeModelId]: { only: ["anthropic"], allowFallbacks: false },
},
}),
},
};
const route = routeModel(config, "openrouter/anthropic-claude-sonnet-5");
expect(route.modelId).toBe(nativeModelId);
expect(passthroughBody(route.provider, route.modelId).provider).toEqual({
only: ["anthropic"], allow_fallbacks: false,
});
});

test("does not forward a caller provider object to non-OpenRouter passthroughs", () => {
expect(passthroughBody(
provider("https://api.deepseek.com/v1"),
"deepseek-chat",
{ provider: { only: ["caller-controlled"] } },
).provider).toBeUndefined();
});

test("preserves provider routing after native Chat key rotation", () => {
const previousHome = process.env.OPENCODEX_HOME;
const home = mkdtempSync(join(tmpdir(), "ocx-openrouter-routing-"));
process.env.OPENCODEX_HOME = home;
clearKeyCooldowns("openrouter");
const openrouter = provider("https://openrouter.ai/api/v1", {
authMode: "key",
apiKey: "key-one",
apiKeyPool: [{ id: "one", key: "key-one" }, { id: "two", key: "key-two" }],
openRouterRouting: { only: ["anthropic"], allowFallbacks: false },
});
const config: OcxConfig = {
port: 10100,
defaultProvider: "openrouter",
providers: { openrouter },
};
try {
const rotated = rotateProviderTransportOn429(config, "openrouter", openrouter, {
attemptedKey: "key-one",
now: 1_000_000,
});
expect(rotated?.apiKey).toBe("key-two");
expect(passthroughBody(rotated!, "anthropic/claude-sonnet-5").provider).toEqual({
only: ["anthropic"], allow_fallbacks: false,
});
} finally {
clearKeyCooldowns("openrouter");
if (previousHome === undefined) delete process.env.OPENCODEX_HOME;
else process.env.OPENCODEX_HOME = previousHome;
removeTreeWithRetry(home);
}
});

test.each([
["https://openrouter.ai/api/v1", "anthropic/claude-sonnet-5", {}],
["https://api.deepseek.com/v1", "deepseek-chat", deepSeekLock],
Expand Down
Loading