intial model list
This commit is contained in:
parent
f63c0b0ba0
commit
8fb442411d
10 changed files with 199 additions and 14 deletions
|
|
@ -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
11
pnpm-lock.yaml
generated
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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']),
|
||||
});
|
||||
|
|
|
|||
35
src/lib/backend/convex/user_enabled_models.ts
Normal file
35
src/lib/backend/convex/user_enabled_models.ts
Normal 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 });
|
||||
},
|
||||
});
|
||||
49
src/lib/backend/models/open-router.ts
Normal file
49
src/lib/backend/models/open-router.ts
Normal 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'
|
||||
);
|
||||
}
|
||||
6
src/routes/+page.server.ts
Normal file
6
src/routes/+page.server.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import { redirect } from "@sveltejs/kit";
|
||||
|
||||
export async function load() {
|
||||
// temporary redirect to /chat
|
||||
redirect(303, '/chat');
|
||||
}
|
||||
|
|
@ -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)}
|
||||
|
|
|
|||
9
src/routes/account/models/+page.server.ts
Normal file
9
src/routes/account/models/+page.server.ts
Normal 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[]),
|
||||
};
|
||||
}
|
||||
20
src/routes/account/models/+page.svelte
Normal file
20
src/routes/account/models/+page.svelte
Normal 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}
|
||||
49
src/routes/account/models/model.svelte
Normal file
49
src/routes/account/models/model.svelte
Normal 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>
|
||||
Loading…
Add table
Reference in a new issue