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
13 changes: 13 additions & 0 deletions packages/backend/src/stores/settings/update.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const createTestModel = (): SettingsModel => ({
},
debugToolsEnabled: false,
autoCreateShiftCollection: true,
openRouterPrioritizeFastProviders: false,
});

describe("settings update", () => {
Expand Down Expand Up @@ -64,6 +65,17 @@ describe("settings update", () => {
expect(result.maxIterations).toBe(50);
});

it("updates openRouterPrioritizeFastProviders", () => {
const model = createTestModel();

const result = update(model, {
type: "UPDATE_SETTINGS",
input: { openRouterPrioritizeFastProviders: true },
});

expect(result.openRouterPrioritizeFastProviders).toBe(true);
});

it("updates renaming config partially", () => {
const model = createTestModel();

Expand Down Expand Up @@ -178,6 +190,7 @@ describe("settings update", () => {
expect(model.maxIterations).toBe(35);
expect(model.renaming.enabled).toBe(false);
expect(model.renaming.renameAfterSend).toBe(false);
expect(model.openRouterPrioritizeFastProviders).toBe(false);
});
});
});
3 changes: 3 additions & 0 deletions packages/backend/src/stores/settings/update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ function handleUpdateSettings(
if (input.autoCreateShiftCollection !== undefined) {
draft.autoCreateShiftCollection = input.autoCreateShiftCollection;
}
if (input.openRouterPrioritizeFastProviders !== undefined) {
draft.openRouterPrioritizeFastProviders = input.openRouterPrioritizeFastProviders;
}
});
}

Expand Down
9 changes: 7 additions & 2 deletions packages/frontend/src/agent/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,12 +71,17 @@ type AgentOptions = {
context: AgentContext;
maxIterations: number;
reasoningEffort: ReasoningEffort;
openRouterPrioritizeFastProviders: boolean;
};

export const createShiftAgent = (options: AgentOptions) => {
const { sdk, model, context, maxIterations, reasoningEffort } = options;
const { sdk, model, context, maxIterations, reasoningEffort, openRouterPrioritizeFastProviders } =
options;

const caidoModel = createModel(sdk, model, { reasoningEffort });
const caidoModel = createModel(sdk, model, {
reasoningEffort,
openRouterPrioritizeFastProviders,
});
const tools = getToolsForMode(context.mode);
const agent = new ToolLoopAgent({
model: caidoModel,
Expand Down
3 changes: 3 additions & 0 deletions packages/frontend/src/agent/transport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,12 +77,15 @@ export class LocalChatTransport implements ChatTransport<ShiftMessage> {

const settingsStore = useSettingsStore();
const maxIterations = settingsStore.maxIterations ?? 35;
const openRouterPrioritizeFastProviders =
settingsStore.openRouterPrioritizeFastProviders ?? false;
const agent = createShiftAgent({
sdk: this.sdk,
model: model,
context: context,
maxIterations,
reasoningEffort: this.store.reasoningEffort,
openRouterPrioritizeFastProviders,
});

const sanitizedMessages = stripReasoningParts(stripUnfinishedToolCalls(options.messages));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,9 @@ watch(
async () => {
if (!isStreaming.value || !hasContent.value) return;
await nextTick();
streamingContainer.value?.scrollTo({
top: streamingContainer.value.scrollHeight,
behavior: "smooth",
});
const container = streamingContainer.value;
if (container === undefined) return;
container.scrollTop = container.scrollHeight;
}
);
</script>
Expand Down Expand Up @@ -73,7 +72,7 @@ watch(
<div
v-if="isStreaming && hasContent && text"
ref="streamingContainer"
class="max-h-48 overflow-y-auto fade-top hide-scrollbar">
class="max-h-48 overflow-y-auto fade-top hide-scrollbar smooth-scroll">
<div class="text-surface-400">
<Markdown :text="text" />
</div>
Expand All @@ -98,6 +97,10 @@ watch(
display: none;
}

.smooth-scroll {
scroll-behavior: smooth;
}

.fade-top {
-webkit-mask-image: linear-gradient(
to top,
Expand Down
20 changes: 20 additions & 0 deletions packages/frontend/src/components/settings/Container.vue
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,15 @@ const autoCreateShiftCollection = computed({
updateSettings(sdk, dispatch, { autoCreateShiftCollection: value });
},
});

const openRouterPrioritizeFastProviders = computed({
get() {
return settingsStore.openRouterPrioritizeFastProviders ?? false;
},
set(value: boolean) {
updateSettings(sdk, dispatch, { openRouterPrioritizeFastProviders: value });
},
});
</script>

<template>
Expand Down Expand Up @@ -93,6 +102,17 @@ const autoCreateShiftCollection = computed({

<ToggleSwitch v-model="autoCreateShiftCollection" />
</div>

<div class="flex items-center justify-between gap-4">
<div class="flex flex-col">
<label class="text-base font-medium">OpenRouter: Prioritize fast providers</label>
<p class="text-sm text-surface-400">
Use the Nitro routing variant (throughput-first). May increase cost.
</p>
</div>

<ToggleSwitch v-model="openRouterPrioritizeFastProviders" />
</div>
</div>
</template>
</Card>
Expand Down
7 changes: 6 additions & 1 deletion packages/frontend/src/float/ai.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,12 @@ export async function queryShift(sdk: FrontendSDK, input: ActionQueryInput): Pro
return Result.err("No models available");
}

const model = createModel(sdk, modelData, { reasoning: false });
const openRouterPrioritizeFastProviders =
settingsStore.openRouterPrioritizeFastProviders ?? false;
const model = createModel(sdk, modelData, {
reasoning: false,
openRouterPrioritizeFastProviders,
});
const learnings = learningsStore.entries;
const prompt = buildPrompt(input, learnings);

Expand Down
5 changes: 5 additions & 0 deletions packages/frontend/src/float/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,11 @@ const getHttpHistoryContext = (

export const getContext = (sdk: FrontendSDK): ActionContext => {
const globalContext = sdk.window.getContext();

if (!isPresent(globalContext)) {
return {};
}

const pageContext = globalContext.page;

const context: ActionContext = {};
Expand Down
4 changes: 3 additions & 1 deletion packages/frontend/src/renaming/ai.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,9 @@ export async function generateName(
return Result.err("No models available");
}

const model = createModel(sdk, modelData);
const openRouterPrioritizeFastProviders =
settingsStore.openRouterPrioritizeFastProviders ?? false;
const model = createModel(sdk, modelData, { openRouterPrioritizeFastProviders });

const prompt = `
<entry>
Expand Down
4 changes: 4 additions & 0 deletions packages/frontend/src/stores/settings/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ export const useSettingsStore = defineStore("settings", () => {
const renaming = computed(() => model.value.config?.renaming);
const debugToolsEnabled = computed(() => model.value.config?.debugToolsEnabled);
const autoCreateShiftCollection = computed(() => model.value.config?.autoCreateShiftCollection);
const openRouterPrioritizeFastProviders = computed(
() => model.value.config?.openRouterPrioritizeFastProviders
);

async function initialize() {
await fetchSettings(sdk, dispatch);
Expand All @@ -45,6 +48,7 @@ export const useSettingsStore = defineStore("settings", () => {
renaming,
debugToolsEnabled,
autoCreateShiftCollection,
openRouterPrioritizeFastProviders,
initialize,
};
});
12 changes: 12 additions & 0 deletions packages/frontend/src/stores/settings/store.update.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,18 @@ describe("settings update", () => {
expect(result.config?.maxIterations).toBe(50);
});

it("updates openRouterPrioritizeFastProviders", () => {
const config = createTestConfig();
const modelWithConfig: SettingsModel = { ...initialModel, config };

const result = update(modelWithConfig, {
type: "UPDATE_SUCCESS",
input: { openRouterPrioritizeFastProviders: true },
});

expect(result.config?.openRouterPrioritizeFastProviders).toBe(true);
});

it("updates renaming config partially", () => {
const config = createTestConfig();
const modelWithConfig: SettingsModel = { ...initialModel, config };
Expand Down
3 changes: 3 additions & 0 deletions packages/frontend/src/stores/settings/store.update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ function handleUpdateSuccess(model: SettingsModel, input: UpdateSettingsInput):
if (input.autoCreateShiftCollection !== undefined) {
draft.config.autoCreateShiftCollection = input.autoCreateShiftCollection;
}
if (input.openRouterPrioritizeFastProviders !== undefined) {
draft.config.openRouterPrioritizeFastProviders = input.openRouterPrioritizeFastProviders;
}
});
}

Expand Down
25 changes: 20 additions & 5 deletions packages/frontend/src/utils/ai.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { type LanguageModelV3 } from "@ai-sdk/provider";
import { type AIUpstreamProviderId } from "@caido/sdk-frontend";
import { createModelKey, type Model, type ModelProvider } from "shared";
import {
createModelKey,
type Model,
ModelProvider,
type ModelProvider as ModelProviderId,
} from "shared";

import type { FrontendSDK } from "@/types";
import { isPresent } from "@/utils/optional";
Expand All @@ -17,7 +22,7 @@ export function getProviderStatuses(sdk: FrontendSDK): ProviderStatus[] {
}));
}

export function isProviderConfigured(sdk: FrontendSDK, provider: ModelProvider): boolean {
export function isProviderConfigured(sdk: FrontendSDK, provider: ModelProviderId): boolean {
const statuses = getProviderStatuses(sdk);
const status = statuses.find((s) => s.id === provider);
return status?.isConfigured ?? false;
Expand All @@ -29,7 +34,7 @@ export function isAnyProviderConfigured(sdk: FrontendSDK): boolean {
}

export class ProviderNotConfiguredError extends Error {
constructor(provider: ModelProvider) {
constructor(provider: ModelProviderId) {
super(`Provider "${provider}" is not configured. Please configure it in Caido AI settings.`);
this.name = "ProviderNotConfiguredError";
}
Expand All @@ -39,22 +44,32 @@ type CreateModelOptions = {
structuredOutput?: boolean;
reasoning?: boolean;
reasoningEffort?: ReasoningEffort;
openRouterPrioritizeFastProviders?: boolean;
};

export type ReasoningEffort = "low" | "medium" | "high";

export function createModel(sdk: FrontendSDK, model: Model, options: CreateModelOptions = {}) {
const { structuredOutput = true, reasoning = true, reasoningEffort = "medium" } = options;
const {
structuredOutput = true,
reasoning = true,
reasoningEffort = "medium",
openRouterPrioritizeFastProviders = false,
} = options;

const isReasoningModel = reasoning && (model?.capabilities.reasoning ?? false);

const provider = sdk.ai.createProvider();

const modelId = model.id.split(":thinking")[0];
let modelId = model.id.split(":thinking")[0];
if (!isPresent(modelId)) {
throw new Error(`Invalid model ID: ${model.id}`);
}

if (model.provider === ModelProvider.OpenRouter && openRouterPrioritizeFastProviders) {
modelId = `${modelId}:nitro`;
}

const modelKey = createModelKey(model.provider, modelId);
const caidoModel = provider(modelKey, {
...(isReasoningModel && {
Expand Down
3 changes: 3 additions & 0 deletions packages/shared/src/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const SettingsConfigSchema = z.object({
renaming: RenamingConfigSchema,
debugToolsEnabled: z.boolean(),
autoCreateShiftCollection: z.boolean(),
openRouterPrioritizeFastProviders: z.boolean(),
});
export type SettingsConfig = z.infer<typeof SettingsConfigSchema>;

Expand All @@ -33,6 +34,7 @@ export const defaultSettingsConfig: SettingsConfig = {
renaming: defaultRenamingConfig,
debugToolsEnabled: false,
autoCreateShiftCollection: true,
openRouterPrioritizeFastProviders: false,
};

export const UpdateSettingsSchema = z.object({
Expand All @@ -43,5 +45,6 @@ export const UpdateSettingsSchema = z.object({
renaming: RenamingConfigSchema.partial().optional(),
debugToolsEnabled: z.boolean().optional(),
autoCreateShiftCollection: z.boolean().optional(),
openRouterPrioritizeFastProviders: z.boolean().optional(),
});
export type UpdateSettingsInput = z.infer<typeof UpdateSettingsSchema>;