diff --git a/packages/backend/src/stores/settings/update.spec.ts b/packages/backend/src/stores/settings/update.spec.ts index 97fec3b..e549697 100644 --- a/packages/backend/src/stores/settings/update.spec.ts +++ b/packages/backend/src/stores/settings/update.spec.ts @@ -15,6 +15,7 @@ const createTestModel = (): SettingsModel => ({ }, debugToolsEnabled: false, autoCreateShiftCollection: true, + openRouterPrioritizeFastProviders: false, }); describe("settings update", () => { @@ -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(); @@ -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); }); }); }); diff --git a/packages/backend/src/stores/settings/update.ts b/packages/backend/src/stores/settings/update.ts index 50ffd37..4d056c5 100644 --- a/packages/backend/src/stores/settings/update.ts +++ b/packages/backend/src/stores/settings/update.ts @@ -27,6 +27,9 @@ function handleUpdateSettings( if (input.autoCreateShiftCollection !== undefined) { draft.autoCreateShiftCollection = input.autoCreateShiftCollection; } + if (input.openRouterPrioritizeFastProviders !== undefined) { + draft.openRouterPrioritizeFastProviders = input.openRouterPrioritizeFastProviders; + } }); } diff --git a/packages/frontend/src/agent/agent.ts b/packages/frontend/src/agent/agent.ts index 015eb69..372d191 100644 --- a/packages/frontend/src/agent/agent.ts +++ b/packages/frontend/src/agent/agent.ts @@ -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, diff --git a/packages/frontend/src/agent/transport.ts b/packages/frontend/src/agent/transport.ts index d95a954..749c0ab 100644 --- a/packages/frontend/src/agent/transport.ts +++ b/packages/frontend/src/agent/transport.ts @@ -77,12 +77,15 @@ export class LocalChatTransport implements ChatTransport { 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)); diff --git a/packages/frontend/src/components/agent/ChatContent/ChatMessage/Assistant/Reasoning/Container.vue b/packages/frontend/src/components/agent/ChatContent/ChatMessage/Assistant/Reasoning/Container.vue index e9aa1a7..89e59b7 100644 --- a/packages/frontend/src/components/agent/ChatContent/ChatMessage/Assistant/Reasoning/Container.vue +++ b/packages/frontend/src/components/agent/ChatContent/ChatMessage/Assistant/Reasoning/Container.vue @@ -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; } ); @@ -73,7 +72,7 @@ watch(
+ class="max-h-48 overflow-y-auto fade-top hide-scrollbar smooth-scroll">
@@ -98,6 +97,10 @@ watch( display: none; } +.smooth-scroll { + scroll-behavior: smooth; +} + .fade-top { -webkit-mask-image: linear-gradient( to top, diff --git a/packages/frontend/src/components/settings/Container.vue b/packages/frontend/src/components/settings/Container.vue index 421b537..ecc032e 100644 --- a/packages/frontend/src/components/settings/Container.vue +++ b/packages/frontend/src/components/settings/Container.vue @@ -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 }); + }, +}); diff --git a/packages/frontend/src/float/ai.ts b/packages/frontend/src/float/ai.ts index 1eb653a..b79eb6d 100644 --- a/packages/frontend/src/float/ai.ts +++ b/packages/frontend/src/float/ai.ts @@ -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); diff --git a/packages/frontend/src/float/context.ts b/packages/frontend/src/float/context.ts index f53297b..c89e47f 100644 --- a/packages/frontend/src/float/context.ts +++ b/packages/frontend/src/float/context.ts @@ -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 = {}; diff --git a/packages/frontend/src/renaming/ai.ts b/packages/frontend/src/renaming/ai.ts index d2bfdbf..0222bdb 100644 --- a/packages/frontend/src/renaming/ai.ts +++ b/packages/frontend/src/renaming/ai.ts @@ -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 = ` diff --git a/packages/frontend/src/stores/settings/store.ts b/packages/frontend/src/stores/settings/store.ts index cd3193d..2a78859 100644 --- a/packages/frontend/src/stores/settings/store.ts +++ b/packages/frontend/src/stores/settings/store.ts @@ -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); @@ -45,6 +48,7 @@ export const useSettingsStore = defineStore("settings", () => { renaming, debugToolsEnabled, autoCreateShiftCollection, + openRouterPrioritizeFastProviders, initialize, }; }); diff --git a/packages/frontend/src/stores/settings/store.update.spec.ts b/packages/frontend/src/stores/settings/store.update.spec.ts index 7b537a3..8d62858 100644 --- a/packages/frontend/src/stores/settings/store.update.spec.ts +++ b/packages/frontend/src/stores/settings/store.update.spec.ts @@ -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 }; diff --git a/packages/frontend/src/stores/settings/store.update.ts b/packages/frontend/src/stores/settings/store.update.ts index 0f166bc..8a9d75a 100644 --- a/packages/frontend/src/stores/settings/store.update.ts +++ b/packages/frontend/src/stores/settings/store.update.ts @@ -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; + } }); } diff --git a/packages/frontend/src/utils/ai.ts b/packages/frontend/src/utils/ai.ts index 0eb3bf3..77520ba 100644 --- a/packages/frontend/src/utils/ai.ts +++ b/packages/frontend/src/utils/ai.ts @@ -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"; @@ -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; @@ -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"; } @@ -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 && { diff --git a/packages/shared/src/settings.ts b/packages/shared/src/settings.ts index d4a3493..93e870a 100644 --- a/packages/shared/src/settings.ts +++ b/packages/shared/src/settings.ts @@ -15,6 +15,7 @@ const SettingsConfigSchema = z.object({ renaming: RenamingConfigSchema, debugToolsEnabled: z.boolean(), autoCreateShiftCollection: z.boolean(), + openRouterPrioritizeFastProviders: z.boolean(), }); export type SettingsConfig = z.infer; @@ -33,6 +34,7 @@ export const defaultSettingsConfig: SettingsConfig = { renaming: defaultRenamingConfig, debugToolsEnabled: false, autoCreateShiftCollection: true, + openRouterPrioritizeFastProviders: false, }; export const UpdateSettingsSchema = z.object({ @@ -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;