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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ The three are designed to work together: search surfaces your notes, the graph r

**🔌 Provider & model choice**

- **Providers:** OpenAI (and any OpenAI-compatible endpoint), Anthropic, Ollama (local), OpenRouter, and OpenAI Codex sign-in. Multiple instances of a provider with distinct names/endpoints are supported.
- **Providers:** OpenAI (and any OpenAI-compatible endpoint), Anthropic, Ollama (local), OpenRouter, [OrcaRouter](https://www.orcarouter.ai), and OpenAI Codex sign-in. Multiple instances of a provider with distinct names/endpoints are supported.
- **Local & private:** With Ollama, models run entirely on your machine and no data leaves it.
- **Quickly switch models:** Change the active model per chat — e.g. one model for scientific writing, another for brainstorming.
- **Fine-grained privacy controls:** Mark individual providers as *trusted* or *untrusted*. Set the vault to **private-by-default** (only notes you explicitly allow can reach a provider) or **public-by-default** (only notes you explicitly exclude are blocked). Untrusted providers — cloud APIs, by default — are blocked from reading, embedding, or being sent private notes, even if the agent tries to access them.
Expand Down
2 changes: 1 addition & 1 deletion src/agent/Agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ const MAX_TEXT_ATTACHMENT_CHARS = 120_000;
const MAX_PDF_EXTRACT_CHARS = 180_000;

/** Providers whose APIs accept native PDF file content blocks. */
export const NATIVE_PDF_PROVIDERS = new Set(["anthropic", "openai", "openrouter"]);
export const NATIVE_PDF_PROVIDERS = new Set(["anthropic", "openai", "openrouter", "orcarouter"]);

function truncateContent(content: string, maxChars: number): string {
if (content.length <= maxChars) return content;
Expand Down
10 changes: 10 additions & 0 deletions src/agent/AgentManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
extractCapabilities as extractOpenRouterCapabilities,
fetchOpenRouterModels,
} from "../providers/openrouterModels";
import { extractOrcaRouterCapabilities, fetchOrcaRouterModels } from "../providers/orcarouterModels";
import type { VisibleNoteRef } from "../hooks/useVisibleNotes.svelte";
import type { SelectionRef } from "../hooks/useSelection.svelte";
import type { GraphNoteRef } from "../stores/chatStore.svelte";
Expand Down Expand Up @@ -248,6 +249,15 @@ async function resolveVisionSupport(providerId: string, modelId: string): Promis
}
}

// 2b. OrcaRouter: derive vision from architecture.input_modalities (cached)
if (providerId === "orcarouter") {
const models = await fetchOrcaRouterModels();
if (models) {
const info = models.get(modelId);
if (info) return extractOrcaRouterCapabilities(info).supportsVision;
}
}

// 3. Fallback: models.dev for other providers (OpenAI, Anthropic, etc.)
const mdInfo = await lookupModelInfo(providerId, modelId);
if (mdInfo) return mdInfo.attachment ?? false;
Expand Down
21 changes: 21 additions & 0 deletions src/components/ui/logos/OrcaRouterLogo.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<script lang="ts">
interface Props {
width?: number;
height?: number;
class?: string;
}

let { width = 16, height = 16, class: className = "" }: Props = $props();
</script>

<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
{width}
{height}
class={className}
fill="currentColor"
fill-rule="evenodd"
>
<path d="M12 2c-4.5 0-8 3.2-8 7.2 0 1.9.8 3.6 2.1 4.9l.6-1.1c-.8-.8-1.3-1.9-1.3-3.1 0-3.3 3-6 6.6-6s6.6 2.7 6.6 6c0 2.3-1.4 4.3-3.4 5.3l1-1.9C17.8 12.5 18.8 10.8 18.8 9c0-3.8-3-7-6.8-7zm0 3.2c-2.6 0-4.7 1.7-4.7 3.8 0 1.1.5 2.1 1.3 2.9l.6-1.1c-.6-.5-1-1.1-1-1.8 0-1.6 1.8-2.9 3.8-2.9s3.8 1.3 3.8 2.9c0 1.3-.9 2.4-2.2 2.8l1-1.9c.7-.5 1.2-1.2 1.2-2.1 0-1.8-1.7-3.2-3.8-3.2zM12 11l-2.2 4.2L12 20l2.2-4.8L12 11z"/>
</svg>
15 changes: 14 additions & 1 deletion src/hooks/useAvailableModels.svelte.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { hydrateChatModel, hydrateEmbeddingModel } from "../lib/modelMetadataNor
import { fetchModelsDevData, type ModelsDevApiResponse } from "../providers/modelsDevApi";
import { getOllamaModelsCache, type OllamaModelInfo } from "../providers/ollamaModels";
import { fetchOpenRouterModels, type OpenRouterModelInfo } from "../providers/openrouterModels";
import { fetchOrcaRouterModels, type OrcaRouterModelInfo } from "../providers/orcarouterModels";
import { getProviderDefinition, isEmbeddingProvider } from "../providers/index";
import type { EmbedModelConfig } from "../providers/index";
import type { ChatModel } from "../stores/chatStore.svelte";
Expand Down Expand Up @@ -64,6 +65,7 @@ export class AvailableModels {
#plugin = getPlugin();
#modelsDevData = $state<ModelsDevApiResponse | null>(null);
#openRouterData = $state<Map<string, OpenRouterModelInfo> | null>(null);
#orcaRouterData = $state<Map<string, OrcaRouterModelInfo> | null>(null);
#metadataLoadStarted = false;

constructor() {
Expand All @@ -75,9 +77,14 @@ export class AvailableModels {
return;
}
this.#metadataLoadStarted = true;
const [modelsDevData, openRouterData] = await Promise.all([fetchModelsDevData(), fetchOpenRouterModels()]);
const [modelsDevData, openRouterData, orcaRouterData] = await Promise.all([
fetchModelsDevData(),
fetchOpenRouterModels(),
fetchOrcaRouterModels(),
]);
this.#modelsDevData = modelsDevData;
this.#openRouterData = openRouterData;
this.#orcaRouterData = orcaRouterData;
}

#getOllamaData(): Map<string, OllamaModelInfo> | null {
Expand Down Expand Up @@ -188,6 +195,7 @@ export class AvailableModels {
hydrateChatModel(model.provider, model.model, {
modelsDevData: this.#modelsDevData,
openRouterData: this.#openRouterData,
orcaRouterData: this.#orcaRouterData,
ollamaData,
temperature: model.modelConfig.temperature,
}),
Expand All @@ -208,6 +216,7 @@ export class AvailableModels {
hydrateEmbeddingModel(model.provider, model.model, {
modelsDevData: this.#modelsDevData,
openRouterData: this.#openRouterData,
orcaRouterData: this.#orcaRouterData,
ollamaData,
similarityThresholdDefault: model.modelConfig.similarityThreshold,
}),
Expand Down Expand Up @@ -296,6 +305,10 @@ export class AvailableModels {
return this.#openRouterData;
}

get orcaRouterModels(): Map<string, OrcaRouterModelInfo> | null {
return this.#orcaRouterData;
}

getOllamaModelInfo(modelId: string): OllamaModelInfo | undefined {
const ollamaData = this.#getOllamaData();
return ollamaData?.get(modelId);
Expand Down
52 changes: 40 additions & 12 deletions src/lib/modelMetadataNormalizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
extractCapabilities as extractOpenRouterCapabilities,
type OpenRouterModelInfo,
} from "../providers/openrouterModels";
import { extractOrcaRouterCapabilities, type OrcaRouterModelInfo } from "../providers/orcarouterModels";
import type { HydratedChatModelMetadata, HydratedEmbeddingModelMetadata } from "../types/modelMetadata";

const DEFAULT_CHAT_CONTEXT_WINDOW = 128000;
Expand All @@ -12,13 +13,15 @@ const DEFAULT_SIMILARITY_THRESHOLD = 0.7;
const DEFAULT_EMBED_MAX_INPUT_TOKENS_BY_PROVIDER: Record<string, number> = {
openai: 8191,
openrouter: 8191,
orcarouter: 8191,
ollama: 8191,
anthropic: 8191,
};

export interface ModelHydrationSourceData {
modelsDevData?: ModelsDevApiResponse | null;
openRouterData?: Map<string, OpenRouterModelInfo> | null;
orcaRouterData?: Map<string, OrcaRouterModelInfo> | null;
ollamaData?: Map<string, OllamaModelInfo> | null;
temperature?: number;
similarityThresholdDefault?: number;
Expand Down Expand Up @@ -83,44 +86,56 @@ function lookupMetadata(
sourceData: ModelHydrationSourceData,
): {
openRouter?: OpenRouterModelInfo;
orcaRouter?: OrcaRouterModelInfo;
ollama?: OllamaModelInfo;
modelsDev?: ReturnType<typeof lookupModelInfoSync>;
} {
const openRouter =
provider === "openrouter" && sourceData.openRouterData ? sourceData.openRouterData.get(variantKey) : undefined;

const orcaRouter =
provider === "orcarouter" && sourceData.orcaRouterData ? sourceData.orcaRouterData.get(variantKey) : undefined;

const ollama = provider === "ollama" && sourceData.ollamaData ? sourceData.ollamaData.get(variantKey) : undefined;

const modelsDev = sourceData.modelsDevData
? lookupModelInfoSync(sourceData.modelsDevData, provider, variantKey)
: null;

return { openRouter, ollama, modelsDev };
return { openRouter, orcaRouter, ollama, modelsDev };
}

export function hydrateChatModel(
provider: string,
variantKey: string,
sourceData: ModelHydrationSourceData = {},
): HydratedChatModelMetadata {
const { openRouter, ollama, modelsDev } = lookupMetadata(provider, variantKey, sourceData);
const { openRouter, orcaRouter, ollama, modelsDev } = lookupMetadata(provider, variantKey, sourceData);
const paramSize = normalizeParamSize(ollama?.parameterSize);
const quantization = ollama?.quantization;

const displayName = buildDisplayName(
provider,
variantKey,
openRouter?.name || ollama?.name || modelsDev?.name,
openRouter?.name || orcaRouter?.name || ollama?.name || modelsDev?.name,
paramSize,
);

const contextWindow =
openRouter?.context_length || ollama?.contextLength || modelsDev?.limit?.context || DEFAULT_CHAT_CONTEXT_WINDOW;
openRouter?.context_length ||
orcaRouter?.context_length ||
ollama?.contextLength ||
modelsDev?.limit?.context ||
DEFAULT_CHAT_CONTEXT_WINDOW;

const inputUsdPer1M = toUsdPer1MFromPerToken(openRouter?.pricing?.prompt);
const outputUsdPer1M = toUsdPer1MFromPerToken(openRouter?.pricing?.completion);
const inputUsdPer1M =
toUsdPer1MFromPerToken(openRouter?.pricing?.prompt) ?? toUsdPer1MFromPerToken(orcaRouter?.pricing?.prompt);
const outputUsdPer1M =
toUsdPer1MFromPerToken(openRouter?.pricing?.completion) ??
toUsdPer1MFromPerToken(orcaRouter?.pricing?.completion);
const hasPricing = inputUsdPer1M !== undefined || outputUsdPer1M !== undefined;
const openRouterCapabilities = openRouter ? extractOpenRouterCapabilities(openRouter) : undefined;
const orcaRouterCapabilities = orcaRouter ? extractOrcaRouterCapabilities(orcaRouter) : undefined;

return {
kind: "chat",
Expand All @@ -133,12 +148,23 @@ export function hydrateChatModel(
temperature: sourceData.temperature,
capabilities: {
toolCalls:
openRouterCapabilities?.supportsToolCalls ?? ollama?.supportsTools ?? modelsDev?.tool_call ?? undefined,
openRouterCapabilities?.supportsToolCalls ??
orcaRouterCapabilities?.supportsToolCalls ??
ollama?.supportsTools ??
modelsDev?.tool_call ??
undefined,
vision:
openRouterCapabilities?.supportsVision ?? ollama?.supportsVision ?? modelsDev?.attachment ?? undefined,
openRouterCapabilities?.supportsVision ??
orcaRouterCapabilities?.supportsVision ??
ollama?.supportsVision ??
modelsDev?.attachment ??
undefined,
reasoning: openRouterCapabilities?.supportsReasoning ?? modelsDev?.reasoning ?? undefined,
structuredOutput:
openRouterCapabilities?.supportsStructuredOutput ?? modelsDev?.structured_output ?? undefined,
openRouterCapabilities?.supportsStructuredOutput ??
orcaRouterCapabilities?.supportsStructuredOutput ??
modelsDev?.structured_output ??
undefined,
},
pricing: hasPricing ? { inputUsdPer1M, outputUsdPer1M } : undefined,
};
Expand All @@ -149,14 +175,14 @@ export function hydrateEmbeddingModel(
variantKey: string,
sourceData: ModelHydrationSourceData = {},
): HydratedEmbeddingModelMetadata {
const { openRouter, ollama, modelsDev } = lookupMetadata(provider, variantKey, sourceData);
const { openRouter, orcaRouter, ollama, modelsDev } = lookupMetadata(provider, variantKey, sourceData);
const paramSize = normalizeParamSize(ollama?.parameterSize);
const quantization = ollama?.quantization;

const displayName = buildDisplayName(
provider,
variantKey,
openRouter?.name || ollama?.name || modelsDev?.name,
openRouter?.name || orcaRouter?.name || ollama?.name || modelsDev?.name,
paramSize,
);

Expand All @@ -167,13 +193,15 @@ export function hydrateEmbeddingModel(

const maxInputTokens =
maxInputFromOpenRouter ||
orcaRouter?.context_length ||
ollama?.contextLength ||
modelsDev?.limit?.input ||
modelsDev?.limit?.context ||
providerDefaultMaxInputTokens ||
DEFAULT_EMBEDDING_MAX_INPUT_TOKENS;

const inputUsdPer1M = toUsdPer1MFromPerToken(openRouter?.pricing?.prompt);
const inputUsdPer1M =
toUsdPer1MFromPerToken(openRouter?.pricing?.prompt) ?? toUsdPer1MFromPerToken(orcaRouter?.pricing?.prompt);

return {
kind: "embedding",
Expand Down
4 changes: 4 additions & 0 deletions src/lib/modelVendorClassification.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,10 @@ export function extractVendor(
return model.model.split("/")[0];
}

if ((model.templateId === "orcarouter" || model.provider === "orcarouter") && model.model.includes("/")) {
return model.model.split("/")[0];
}

if (model.templateId === "openai-codex" || model.provider === "openai") {
return "openai";
}
Expand Down
13 changes: 13 additions & 0 deletions src/providers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { anthropicProvider } from "./anthropic";
import { ollamaProvider } from "./ollama";
import { openaiProvider } from "./openai";
import { openrouterProvider } from "./openrouter";
import { orcarouterProvider } from "./orcarouter";
import { openAICodexProvider } from "./openai-codex";
import { createOmlxProvider } from "./omlx";

Expand Down Expand Up @@ -51,6 +52,11 @@ export const PROVIDER_TEMPLATES: readonly ProviderTemplateDefinition[] = [
displayName: "OpenRouter",
description: "OpenRouter account and model catalog.",
},
{
id: "orcarouter",
displayName: "OrcaRouter",
description: "OrcaRouter gateway with 200+ frontier models.",
},
] as const;

export function getProviderTemplate(templateId: ProviderTemplateId): ProviderTemplateDefinition | undefined {
Expand Down Expand Up @@ -97,6 +103,12 @@ function createTemplateDefinition(
id: instanceId,
displayName: meta.displayName,
};
case "orcarouter":
return {
...orcarouterProvider,
id: instanceId,
displayName: meta.displayName,
};
default:
return undefined;
}
Expand Down Expand Up @@ -167,3 +179,4 @@ export { anthropicProvider } from "./anthropic";
export { openaiProvider } from "./openai";
export { ollamaProvider } from "./ollama";
export { openrouterProvider } from "./openrouter";
export { orcarouterProvider } from "./orcarouter";
Loading