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",
"melt": "^0.35.0",
"mode-watcher": "^1.0.8",
"neverthrow": "^8.2.0",
"prettier": "^3.4.2",
"prettier-plugin-svelte": "^3.3.3",
"prettier-plugin-tailwindcss": "^0.6.11",

11
pnpm-lock.yaml generated
View file

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

View file

@ -6,10 +6,20 @@ export const providerValidator = v.union(...Object.values(Provider).map((p) => v
export default defineSchema({
user_keys: defineTable({
provider: providerValidator,
user_id: v.id('users'),
provider: providerValidator,
key: v.string(),
})
.index('by_user', ['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">
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 { useConvexClient, useQuery } from 'convex-svelte';
import { useQuery } from 'convex-svelte';
import { api } from '$lib/backend/convex/_generated/api';
import { session } from '$lib/state/session.svelte.js';
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 ?? '' });
</script>
<h1 class="text-2xl font-bold">API Keys</h1>
<h2 class="text-muted-foreground mt-2 text-sm">
<div>
<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
towards your monthly limits.
</h2>
</h2>
</div>
<div class="mt-8 flex flex-col gap-8">
{#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>