Skip to content
Open
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
1 change: 1 addition & 0 deletions core/control-plane/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const modelDescriptionSchema = z.object({
"nebius",
"siliconflow",
"tensorix",
"orcarouter",
"scaleway",
"watsonx",
]),
Expand Down
2 changes: 2 additions & 0 deletions core/llm/autodetect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ const PROVIDER_HANDLES_TEMPLATING: string[] = [
"nebius",
"relace",
"openrouter",
"orcarouter",
"clawrouter",
"deepseek",
"xAI",
Expand Down Expand Up @@ -124,6 +125,7 @@ const PROVIDER_SUPPORTS_IMAGES: string[] = [
"sagemaker",
"continue-proxy",
"openrouter",
"orcarouter",
"clawrouter",
"venice",
"sambanova",
Expand Down
138 changes: 138 additions & 0 deletions core/llm/llms/OrcaRouter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import { ChatCompletionCreateParams } from "openai/resources/index";

import { ORCAROUTER_HEADERS } from "@continuedev/openai-adapters";

import { LLMOptions } from "../../index.js";
import { osModelsEditPrompt } from "../templates/edit.js";

import OpenAI from "./OpenAI.js";

class OrcaRouter extends OpenAI {
static providerName = "orcarouter";
protected supportsReasoningField = true;
protected supportsReasoningDetailsField = true;
static defaultOptions: Partial<LLMOptions> = {
apiBase: "https://api.orcarouter.ai/v1/",
model: "orcarouter/auto",
promptTemplates: {
edit: osModelsEditPrompt,
},
useLegacyCompletionsEndpoint: false,
};

constructor(options: LLMOptions) {
super({
...options,
requestOptions: {
...options.requestOptions,
headers: {
...ORCAROUTER_HEADERS,
...options.requestOptions?.headers,
},
},
});
}

private isAnthropicModel(model?: string): boolean {
if (!model) return false;
return model.toLowerCase().includes("claude");
}

private addCacheControlToContent(content: any, addCaching: boolean): any {
if (!addCaching) return content;

if (typeof content === "string") {
return [
{
type: "text",
text: content,
cache_control: { type: "ephemeral" },
},
];
}

if (Array.isArray(content)) {
return content.map((part, idx) => {
if (part.type === "text" && idx === content.length - 1) {
return {
...part,
cache_control: { type: "ephemeral" },
};
}
return part;
});
}

return content;
}

protected modifyChatBody(
body: ChatCompletionCreateParams,
): ChatCompletionCreateParams {
body = super.modifyChatBody(body);

if (
!this.isAnthropicModel(body.model) ||
(!this.cacheBehavior && !this.completionOptions.promptCaching)
) {
return body;
}

const shouldCacheConversation =
this.cacheBehavior?.cacheConversation ||
this.completionOptions.promptCaching;
const shouldCacheSystemMessage =
this.cacheBehavior?.cacheSystemMessage ||
this.completionOptions.promptCaching;

if (!shouldCacheConversation && !shouldCacheSystemMessage) {
return body;
}

const filteredMessages = body.messages.filter(
(m: any) => m.role !== "system" && !!m.content,
);

const lastTwoUserMsgIndices = filteredMessages
.map((msg: any, index: number) => (msg.role === "user" ? index : -1))
.filter((index: number) => index !== -1)
.slice(-2);

let filteredIndex = 0;
const filteredToOriginalIndexMap: number[] = [];
body.messages.forEach((msg: any, originalIndex: number) => {
if (msg.role !== "system" && !!msg.content) {
filteredToOriginalIndexMap[filteredIndex] = originalIndex;
filteredIndex++;
}
});

body.messages = body.messages.map((message: any, idx) => {
if (message.role === "system" && shouldCacheSystemMessage) {
return {
...message,
content: this.addCacheControlToContent(message.content, true),
};
}

const filteredIdx = filteredToOriginalIndexMap.indexOf(idx);
if (
message.role === "user" &&
shouldCacheConversation &&
filteredIdx !== -1 &&
lastTwoUserMsgIndices.includes(filteredIdx)
) {
return {
...message,
content: this.addCacheControlToContent(message.content, true),
};
}

return message;
});

return body;
}
}

export default OrcaRouter;
174 changes: 174 additions & 0 deletions core/llm/llms/OrcaRouter.vitest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
import { ChatCompletionCreateParams } from "openai/resources/index";
import { describe, expect, it } from "vitest";

import OrcaRouter from "./OrcaRouter";

describe("OrcaRouter", () => {
it("uses the correct providerName and default apiBase", () => {
expect(OrcaRouter.providerName).toBe("orcarouter");
expect(OrcaRouter.defaultOptions?.apiBase).toBe(
"https://api.orcarouter.ai/v1/",
);
expect(OrcaRouter.defaultOptions?.model).toBe("orcarouter/auto");
});

it("injects OrcaRouter attribution headers", () => {
const orcaRouter = new OrcaRouter({
model: "orcarouter/auto",
apiKey: "sk-orca-test",
});

const headers = (orcaRouter as any).requestOptions?.headers ?? {};
expect(headers["HTTP-Referer"]).toBe("https://www.continue.dev/");
expect(headers["X-Title"]).toBe("Continue");
expect(headers["X-Continue-Provider"]).toBe("orcarouter");
});

it("allows user-provided headers to override defaults", () => {
const orcaRouter = new OrcaRouter({
model: "orcarouter/auto",
apiKey: "sk-orca-test",
requestOptions: {
headers: { "X-Title": "MyApp" },
},
});

const headers = (orcaRouter as any).requestOptions?.headers ?? {};
expect(headers["X-Title"]).toBe("MyApp");
});
});

describe("OrcaRouter Anthropic Caching", () => {
it("does not throw for Anthropic models without cacheBehavior", () => {
const orcaRouter = new OrcaRouter({
model: "anthropic/claude-opus-4.7",
apiKey: "sk-orca-test",
});

const body: ChatCompletionCreateParams = {
model: "anthropic/claude-opus-4.7",
messages: [],
};

expect(() => orcaRouter["modifyChatBody"](body)).not.toThrow();
});

it("adds cache_control to last two user messages when caching is enabled", () => {
const orcaRouter = new OrcaRouter({
model: "anthropic/claude-opus-4.7",
apiKey: "sk-orca-test",
cacheBehavior: {
cacheConversation: true,
cacheSystemMessage: false,
},
});

const body: ChatCompletionCreateParams = {
model: "anthropic/claude-opus-4.7",
messages: [
{ role: "user", content: "First message" },
{ role: "assistant", content: "Response" },
{ role: "user", content: "Second message" },
{ role: "assistant", content: "Another response" },
{ role: "user", content: "Third message" },
],
};

const modifiedBody = orcaRouter["modifyChatBody"](body);
const userMessages = modifiedBody.messages.filter(
(msg: any) => msg.role === "user",
);

expect(userMessages[0].content).toBe("First message");
expect(userMessages[1].content).toEqual([
{
type: "text",
text: "Second message",
cache_control: { type: "ephemeral" },
},
]);
expect(userMessages[2].content).toEqual([
{
type: "text",
text: "Third message",
cache_control: { type: "ephemeral" },
},
]);
});

it("adds cache_control to system message when caching is enabled", () => {
const orcaRouter = new OrcaRouter({
model: "anthropic/claude-opus-4.7",
apiKey: "sk-orca-test",
cacheBehavior: {
cacheConversation: false,
cacheSystemMessage: true,
},
});

const body: ChatCompletionCreateParams = {
model: "anthropic/claude-opus-4.7",
messages: [
{ role: "system", content: "You are a helpful assistant" },
{ role: "user", content: "Hello" },
],
};

const modifiedBody = orcaRouter["modifyChatBody"](body);

expect(modifiedBody.messages[0]).toEqual({
role: "system",
content: [
{
type: "text",
text: "You are a helpful assistant",
cache_control: { type: "ephemeral" },
},
],
});
expect(modifiedBody.messages[1]).toEqual({
role: "user",
content: "Hello",
});
});

it("does not modify messages for non-Anthropic models", () => {
const orcaRouter = new OrcaRouter({
model: "openai/gpt-5.5",
apiKey: "sk-orca-test",
cacheBehavior: {
cacheConversation: true,
cacheSystemMessage: true,
},
});

const body: ChatCompletionCreateParams = {
model: "openai/gpt-5.5",
messages: [
{ role: "system", content: "System message" },
{ role: "user", content: "User message" },
],
};

const modifiedBody = orcaRouter["modifyChatBody"](body);
expect(modifiedBody.messages).toEqual(body.messages);
});

it("does not modify messages when no caching is enabled", () => {
const orcaRouter = new OrcaRouter({
model: "anthropic/claude-opus-4.7",
apiKey: "sk-orca-test",
});

const body: ChatCompletionCreateParams = {
model: "anthropic/claude-opus-4.7",
messages: [
{ role: "system", content: "System message" },
{ role: "user", content: "User message" },
],
};

const modifiedBody = orcaRouter["modifyChatBody"](body);
expect(modifiedBody.messages).toEqual(body.messages);
});
});
2 changes: 2 additions & 0 deletions core/llm/llms/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import Nvidia from "./Nvidia";
import Ollama from "./Ollama";
import OpenAI from "./OpenAI";
import OpenRouter from "./OpenRouter";
import OrcaRouter from "./OrcaRouter";
import ClawRouter from "./ClawRouter";
import OVHcloud from "./OVHcloud";
import { Relace } from "./Relace";
Expand Down Expand Up @@ -112,6 +113,7 @@ export const LLMClasses = [
Azure,
WatsonX,
OpenRouter,
OrcaRouter,
ClawRouter,
Nvidia,
Vllm,
Expand Down
Loading
Loading