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",
|
"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
11
pnpm-lock.yaml
generated
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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']),
|
||||||
});
|
});
|
||||||
|
|
|
||||||
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">
|
<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>
|
||||||
Bring your own API keys for select models. Messages sent using your API keys will not count
|
<h2 class="text-muted-foreground mt-2 text-sm">
|
||||||
towards your monthly limits.
|
Bring your own API keys for select models. Messages sent using your API keys will not count
|
||||||
</h2>
|
towards your monthly limits.
|
||||||
|
</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)}
|
||||||
|
|
|
||||||
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