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
15 changes: 15 additions & 0 deletions core/llm/llms/OpenRouter.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { ChatCompletionCreateParams } from "openai/resources/index";

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

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

Expand All @@ -18,6 +20,19 @@ class OpenRouter extends OpenAI {
useLegacyCompletionsEndpoint: false,
};

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

private isAnthropicModel(model?: string): boolean {
if (!model) return false;
const modelLower = model.toLowerCase();
Expand Down
8 changes: 8 additions & 0 deletions core/util/history.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,14 @@ describe("Workspace directory filtering", () => {
expect(sessions.length).toBe(4);
});

test("Empty workspace filter only returns no-workspace sessions", () => {
const sessions = historyManager.list({
workspaceDirectory: "",
});
expect(sessions.length).toBe(1);
expect(sessions[0].sessionId).toBe("ws-none");
});

test("Workspace filter is case-insensitive", () => {
const sessions = historyManager.list({
workspaceDirectory: "/HOME/USER/PROJECT-A",
Expand Down
5 changes: 4 additions & 1 deletion core/util/history.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,10 @@ export class HistoryManager {
.reverse();

// Filter by workspace directory if provided
if (options.workspaceDirectory) {
if (
options.workspaceDirectory !== undefined &&
options.workspaceDirectory !== null
) {
const target = options.workspaceDirectory.toLowerCase();
sessions = sessions.filter(
(session) =>
Expand Down
2 changes: 1 addition & 1 deletion extensions/cli/src/commands/BaseCommandOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export interface BaseCommandOptions {
agent?: string;
/** Enable beta UploadArtifact tool */
betaUploadArtifactTool?: boolean;
/** Enable beta Subagent tool */
/** Enable beta Subagent tool with built-in specialized agents */
betaSubagentTool?: boolean;
}

Expand Down
2 changes: 1 addition & 1 deletion extensions/cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ addCommonOptions(program)
.option("--fork <sessionId>", "Fork from an existing session ID")
.option(
"--beta-subagent-tool",
"Enable beta Subagent tool for invoking subagents",
"Enable beta Subagent tool with built-in specialized agents",
)
.action(async (prompt, options) => {
// Telemetry: record command invocation
Expand Down
88 changes: 88 additions & 0 deletions extensions/cli/src/services/ModelService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { AuthConfig } from "../auth/workos.js";
import * as config from "../config.js";

import { ModelService } from "./ModelService.js";
import type { ModelServiceState } from "./types.js";

describe("ModelService", () => {
let service: ModelService;
Expand Down Expand Up @@ -373,3 +374,90 @@ describe("ModelService", () => {
});
});
});

describe("ModelService.getSubagentModels", () => {
const baseState: ModelServiceState = {
llmApi: { provider: "primary-llm" } as any,
model: {
name: "primary-chat-model",
provider: "openai",
model: "gpt-4.1",
roles: ["chat"],
chatOptions: {},
},
assistant: {
models: [
{
name: "primary-chat-model",
provider: "openai",
model: "gpt-4.1",
roles: ["chat"],
chatOptions: {},
},
],
} as any,
authConfig: null,
};

beforeEach(() => {
vi.mocked(config.createLlmApi).mockReturnValue({
provider: "mock-llm",
} as any);
});

test("returns configured subagents when present", () => {
const state: ModelServiceState = {
...baseState,
assistant: {
models: [
...(baseState.assistant?.models ?? []),
{
name: "security-review",
provider: "openai",
model: "gpt-4.1",
roles: ["subagent"],
chatOptions: {
baseSystemMessage: "Review code for security issues.",
},
},
],
} as any,
};

const subagents = ModelService.getSubagentModels(state);

expect(subagents.map((subagent) => subagent.model?.name)).toEqual([
"security-review",
]);
});

test("falls back to built-in subagents when none are configured", () => {
const subagents = ModelService.getSubagentModels(baseState);

expect(subagents.map((subagent) => subagent.model?.name)).toEqual([
"planner",
"researcher",
"reviewer",
]);
expect(
subagents.every((subagent) => subagent.model?.model === "gpt-4.1"),
).toBe(true);
expect(
subagents.every((subagent) => subagent.model?.provider === "openai"),
).toBe(true);
expect(
subagents.every((subagent) =>
subagent.model?.roles?.includes("subagent"),
),
).toBe(true);
});

test("returns no subagents when there is no current model to clone", () => {
const subagents = ModelService.getSubagentModels({
...baseState,
model: null,
});

expect(subagents).toEqual([]);
});
});
29 changes: 14 additions & 15 deletions extensions/cli/src/services/ModelService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { AssistantUnrolled, ModelConfig } from "@continuedev/config-yaml";

import { AuthConfig, getModelName } from "../auth/workos.js";
import { createLlmApi, getLlmApi } from "../config.js";
import { getBuiltInSubagentModels } from "../subagent/builtins.js";
import { logger } from "../util/logger.js";

import { BaseService, ServiceWithDependencies } from "./BaseService.js";
Expand Down Expand Up @@ -307,23 +308,21 @@ export class ModelService
}

static getSubagentModels(modelState: ModelServiceState) {
if (!modelState.assistant) {
return [];
}
const subagentModels = modelState.assistant.models
const configuredSubagentModels = modelState.assistant?.models
?.filter((model) => !!model)
.filter((model) => !!model.name) // filter out models without a name
.filter((model) => model.roles?.includes("subagent")) // filter with role subagent
.filter((model) => !!model.chatOptions?.baseSystemMessage); // filter those with a system message
.filter((model) => !!model.name)
.filter((model) => model.roles?.includes("subagent"))
.filter((model) => !!model.chatOptions?.baseSystemMessage);

if (!subagentModels) {
return [];
if (configuredSubagentModels?.length) {
return configuredSubagentModels.map((model) => ({
llmApi: createLlmApi(model, modelState.authConfig),
model,
assistant: modelState.assistant,
authConfig: modelState.authConfig,
}));
}
return subagentModels?.map((model) => ({
llmApi: createLlmApi(model, modelState.authConfig),
model,
assistant: modelState.assistant,
authConfig: modelState.authConfig,
}));

return getBuiltInSubagentModels(modelState);
}
}
61 changes: 61 additions & 0 deletions extensions/cli/src/subagent/builtins.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { ModelConfig, ModelRole } from "@continuedev/config-yaml";

import { createLlmApi } from "../config.js";
import type { ModelServiceState } from "../services/types.js";

interface BuiltInSubagentDefinition {
name: string;
baseSystemMessage: string;
}

const BUILT_IN_SUBAGENT_DEFINITIONS: BuiltInSubagentDefinition[] = [
{
name: "planner",
baseSystemMessage:
"Planner. Investigate the task, identify the most relevant files, constraints, and validation steps, then return a concise execution plan. Do not make unrelated edits.",
},
{
name: "researcher",
baseSystemMessage:
"Researcher. Gather the most relevant code, configuration, and documentation context for the task. Summarize concrete findings, tradeoffs, and unknowns with evidence from the repo.",
},
{
name: "reviewer",
baseSystemMessage:
"Reviewer. Review the proposed implementation for correctness, regressions, missing tests, and edge cases. Prioritize actionable findings and be specific.",
},
];

function createBuiltInSubagentModel(
baseModel: ModelConfig,
definition: BuiltInSubagentDefinition,
): ModelConfig {
return {
...baseModel,
name: definition.name,
roles: [...new Set<ModelRole>([...(baseModel.roles ?? []), "subagent"])],
chatOptions: {
...baseModel.chatOptions,
baseSystemMessage: definition.baseSystemMessage,
},
};
}

export function getBuiltInSubagentModels(
modelState: ModelServiceState,
): ModelServiceState[] {
const baseModel = modelState.model;
if (!baseModel) {
return [];
}

return BUILT_IN_SUBAGENT_DEFINITIONS.map((definition) => {
const model = createBuiltInSubagentModel(baseModel, definition);
return {
llmApi: createLlmApi(model, modelState.authConfig),
model,
assistant: modelState.assistant,
authConfig: modelState.authConfig,
};
}).filter((subagentModel) => !!subagentModel.llmApi);
}
10 changes: 5 additions & 5 deletions extensions/cli/src/subagent/get-agents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import type { ModelServiceState } from "../services/types.js";
export function getSubagent(modelState: ModelServiceState, name: string) {
return (
ModelService.getSubagentModels(modelState).find(
(model) => model.model.name === name,
(model) => model.model?.name === name,
) ?? null
);
}
Expand All @@ -21,7 +21,7 @@ export function generateSubagentToolDescription(
const agentList = ModelService.getSubagentModels(modelState)
.map(
(subagentModel) =>
` - ${subagentModel.model.name}: ${subagentModel.model.chatOptions?.baseSystemMessage}`,
` - ${subagentModel.model?.name ?? "unknown"}: ${subagentModel.model?.chatOptions?.baseSystemMessage ?? ""}`,
)
.join("\n");

Expand All @@ -34,7 +34,7 @@ ${agentList}
}

export function getAgentNames(modelState: ModelServiceState): string[] {
return ModelService.getSubagentModels(modelState).map(
(model) => model.model.name,
);
return ModelService.getSubagentModels(modelState)
.map((model) => model.model?.name)
.filter((name): name is string => !!name);
}
2 changes: 1 addition & 1 deletion extensions/cli/src/tools/subagent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export const subagentTool = async (): Promise<Tool> => {
preview: [
{
type: "text",
content: `Spawning ${agent.model.name} to: ${description}`,
content: `Spawning ${agent.model?.name ?? subagent_name} to: ${description}`,
},
],
};
Expand Down
5 changes: 3 additions & 2 deletions packages/openai-adapters/src/apis/OpenRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@ export interface OpenRouterConfig extends OpenAIConfig {

// TODO: Extract detailed error info from OpenRouter's error.metadata.raw to surface better messages

const OPENROUTER_HEADERS: Record<string, string> = {
export const OPENROUTER_HEADERS: Record<string, string> = {
"HTTP-Referer": "https://www.continue.dev/",
"X-Title": "Continue",
"X-OpenRouter-Title": "Continue",
"X-OpenRouter-Categories": "ide-extension",
};

export class OpenRouterApi extends OpenAIApi {
Expand Down
1 change: 1 addition & 0 deletions packages/openai-adapters/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -243,4 +243,5 @@ export {
} from "./apis/AnthropicUtils.js";

export { isResponsesModel } from "./apis/openaiResponses.js";
export { OPENROUTER_HEADERS } from "./apis/OpenRouter.js";
export { extractBase64FromDataUrl, parseDataUrl } from "./util/url.js";
Loading