intial model list

This commit is contained in:
Aidan Bleser 2025-06-14 07:35:32 -05:00
parent f63c0b0ba0
commit 8fb442411d
10 changed files with 199 additions and 14 deletions

View file

@ -41,6 +41,7 @@
"jsdom": "^26.0.0", "jsdom": "^26.0.0",
"melt": "^0.35.0", "melt": "^0.35.0",
"mode-watcher": "^1.0.8", "mode-watcher": "^1.0.8",
"neverthrow": "^8.2.0",
"prettier": "^3.4.2", "prettier": "^3.4.2",
"prettier-plugin-svelte": "^3.3.3", "prettier-plugin-svelte": "^3.3.3",
"prettier-plugin-tailwindcss": "^0.6.11", "prettier-plugin-tailwindcss": "^0.6.11",

11
pnpm-lock.yaml generated
View file

@ -84,6 +84,9 @@ importers:
mode-watcher: mode-watcher:
specifier: ^1.0.8 specifier: ^1.0.8
version: 1.0.8(svelte@5.34.1) version: 1.0.8(svelte@5.34.1)
neverthrow:
specifier: ^8.2.0
version: 8.2.0
prettier: prettier:
specifier: ^3.4.2 specifier: ^3.4.2
version: 3.5.3 version: 3.5.3
@ -1745,6 +1748,10 @@ packages:
natural-compare@1.4.0: natural-compare@1.4.0:
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
neverthrow@8.2.0:
resolution: {integrity: sha512-kOCT/1MCPAxY5iUV3wytNFUMUolzuwd/VF/1KCx7kf6CutrOsTie+84zTGTpgQycjvfLdBBdvBvFLqFD2c0wkQ==}
engines: {node: '>=18'}
nwsapi@2.2.20: nwsapi@2.2.20:
resolution: {integrity: sha512-/ieB+mDe4MrrKMT8z+mQL8klXydZWGR5Dowt4RAGKbJ3kIGEx3X4ljUo+6V73IXtUPWgfOlU5B9MlGxFO5T+cA==} resolution: {integrity: sha512-/ieB+mDe4MrrKMT8z+mQL8klXydZWGR5Dowt4RAGKbJ3kIGEx3X4ljUo+6V73IXtUPWgfOlU5B9MlGxFO5T+cA==}
@ -3852,6 +3859,10 @@ snapshots:
natural-compare@1.4.0: {} natural-compare@1.4.0: {}
neverthrow@8.2.0:
optionalDependencies:
'@rollup/rollup-linux-x64-gnu': 4.43.0
nwsapi@2.2.20: {} nwsapi@2.2.20: {}
optionator@0.9.4: optionator@0.9.4:

View file

@ -6,10 +6,20 @@ export const providerValidator = v.union(...Object.values(Provider).map((p) => v
export default defineSchema({ export default defineSchema({
user_keys: defineTable({ user_keys: defineTable({
provider: providerValidator,
user_id: v.id('users'), user_id: v.id('users'),
provider: providerValidator,
key: v.string(), key: v.string(),
}) })
.index('by_user', ['user_id']) .index('by_user', ['user_id'])
.index('by_provider_user', ['provider', 'user_id']), .index('by_provider_user', ['provider', 'user_id']),
user_enabled_models: defineTable({
user_id: v.id('users'),
provider: providerValidator,
/** Different providers may use different ids for the same model */
model_id: v.string(),
pinned: v.union(v.number(), v.null())
})
.index('by_user', ['user_id'])
.index('by_model_provider', ['model_id', 'provider'])
.index('by_provider_user', ['provider', 'user_id']),
}); });

View file

@ -0,0 +1,35 @@
import { mutation } from './_generated/server';
import { v } from 'convex/values';
import { providerValidator } from './schema';
export const get = mutation({
args: {
user_id: v.id('users'),
},
handler: async (ctx, args) => {
return await ctx.db
.query('user_enabled_models')
.withIndex('by_user', (q) => q.eq('user_id', args.user_id))
.collect();
},
});
export const set = mutation({
args: {
provider: providerValidator,
model_id: v.string(),
user_id: v.id('users'),
},
handler: async (ctx, args) => {
const existing = await ctx.db
.query('user_enabled_models')
.withIndex('by_model_provider', (q) =>
q.eq('model_id', args.model_id).eq('provider', args.provider)
)
.first();
if (existing) return;
await ctx.db.insert('user_enabled_models', { ...args, pinned: null });
},
});

View file

@ -0,0 +1,49 @@
import { ResultAsync } from 'neverthrow';
export interface OpenRouterModel {
id: string;
name: string;
created: number;
description: string;
architecture: OpenRouterArchitecture;
top_provider: OpenRouterTopProvider;
pricing: OpenRouterPricing;
context_length: number;
hugging_face_id: string;
per_request_limits: Record<string, string>;
supported_parameters: string[];
}
interface OpenRouterArchitecture {
input_modalities: string[];
output_modalities: string[];
tokenizer: string;
}
interface OpenRouterTopProvider {
is_moderated: boolean;
}
interface OpenRouterPricing {
prompt: string;
completion: string;
image: string;
request: string;
input_cache_read: string;
input_cache_write: string;
web_search: string;
internal_reasoning: string;
}
export function getOpenRouterModels() {
return ResultAsync.fromPromise(
(async () => {
const res = await fetch('https://openrouter.ai/api/v1/models');
const { data } = await res.json()
return data as OpenRouterModel[];
})(),
() => '[open-router] Failed to fetch models'
);
}

View file

@ -0,0 +1,6 @@
import { redirect } from "@sveltejs/kit";
export async function load() {
// temporary redirect to /chat
redirect(303, '/chat');
}

View file

@ -1,11 +1,6 @@
<script lang="ts"> <script lang="ts">
import * as Card from '$lib/components/ui/card';
import { KeyIcon } from '@lucide/svelte';
import { Input } from '$lib/components/ui/input';
import { Button } from '$lib/components/ui/button';
import { Link } from '$lib/components/ui/link';
import { Provider } from '$lib/types'; import { Provider } from '$lib/types';
import { useConvexClient, useQuery } from 'convex-svelte'; import { useQuery } from 'convex-svelte';
import { api } from '$lib/backend/convex/_generated/api'; import { api } from '$lib/backend/convex/_generated/api';
import { session } from '$lib/state/session.svelte.js'; import { session } from '$lib/state/session.svelte.js';
import ProviderCard from './provider-card.svelte'; import ProviderCard from './provider-card.svelte';
@ -57,16 +52,16 @@
}, },
}; };
const client = useConvexClient();
const keys = useQuery(api.user_keys.get, { user_id: session.current?.user.id ?? '' }); const keys = useQuery(api.user_keys.get, { user_id: session.current?.user.id ?? '' });
</script> </script>
<h1 class="text-2xl font-bold">API Keys</h1> <div>
<h2 class="text-muted-foreground mt-2 text-sm"> <h1 class="text-2xl font-bold">API Keys</h1>
<h2 class="text-muted-foreground mt-2 text-sm">
Bring your own API keys for select models. Messages sent using your API keys will not count Bring your own API keys for select models. Messages sent using your API keys will not count
towards your monthly limits. towards your monthly limits.
</h2> </h2>
</div>
<div class="mt-8 flex flex-col gap-8"> <div class="mt-8 flex flex-col gap-8">
{#each allProviders as provider (provider)} {#each allProviders as provider (provider)}

View file

@ -0,0 +1,9 @@
import { getOpenRouterModels, type OpenRouterModel } from '$lib/backend/models/open-router';
export async function load() {
const [openRouterModels] = await Promise.all([getOpenRouterModels()]);
return {
openRouterModels: openRouterModels.unwrapOr([] as OpenRouterModel[]),
};
}

View file

@ -0,0 +1,20 @@
<script lang="ts">
import { Provider } from '$lib/types.js';
import { useConvexClient } from 'convex-svelte';
import Model from './model.svelte';
let { data } = $props();
const client = useConvexClient();
</script>
<div>
<h1 class="text-2xl font-bold">Available Models</h1>
<h2 class="text-muted-foreground mt-2 text-sm">
Choose which models appear in your model selector. This won't affect existing conversations.
</h2>
</div>
{#each data.openRouterModels as model}
<Model provider={Provider.OpenRouter} model={model} />
{/each}

View file

@ -0,0 +1,49 @@
<script lang="ts">
import type { Provider } from '$lib/types';
import * as Card from '$lib/components/ui/card';
type Model = {
id: string;
name: string;
description: string;
};
type Props = {
provider: Provider;
model: Model;
};
let { provider, model }: Props = $props();
function getShortDescription(text: string) {
// match any punctuation followed by a space or the end of the string
const index = text.match(/[.!?](\s|$)/)?.index;
if (index === undefined) return { shortDescription: null, fullDescription: text };
return { shortDescription: text.slice(0, index + 1), fullDescription: text };
}
const { shortDescription, fullDescription } = $derived(getShortDescription(model.description));
let showMore = $state(false);
</script>
<Card.Root>
<Card.Header>
<Card.Title>{model.name}</Card.Title>
<Card.Description>{showMore ? fullDescription : shortDescription ?? fullDescription}</Card.Description>
{#if shortDescription !== null}
<button
type="button"
class="text-muted-foreground text-start w-fit text-xs"
onclick={() => (showMore = !showMore)}
>
{showMore ? 'Show less' : 'Show more'}
</button>
{/if}
</Card.Header>
<Card.Content>
</Card.Content>
</Card.Root>