diff --git a/package.json b/package.json index 8d4da65..7c91ec8 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d37171b..89dd834 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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: diff --git a/src/lib/backend/convex/schema.ts b/src/lib/backend/convex/schema.ts index 163cb71..a652632 100644 --- a/src/lib/backend/convex/schema.ts +++ b/src/lib/backend/convex/schema.ts @@ -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']), }); diff --git a/src/lib/backend/convex/user_enabled_models.ts b/src/lib/backend/convex/user_enabled_models.ts new file mode 100644 index 0000000..4bc2f47 --- /dev/null +++ b/src/lib/backend/convex/user_enabled_models.ts @@ -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 }); + }, +}); diff --git a/src/lib/backend/models/open-router.ts b/src/lib/backend/models/open-router.ts new file mode 100644 index 0000000..13988db --- /dev/null +++ b/src/lib/backend/models/open-router.ts @@ -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; + 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' + ); +} diff --git a/src/routes/+page.server.ts b/src/routes/+page.server.ts new file mode 100644 index 0000000..9048099 --- /dev/null +++ b/src/routes/+page.server.ts @@ -0,0 +1,6 @@ +import { redirect } from "@sveltejs/kit"; + +export async function load() { + // temporary redirect to /chat + redirect(303, '/chat'); +} \ No newline at end of file diff --git a/src/routes/account/api-keys/+page.svelte b/src/routes/account/api-keys/+page.svelte index 27cdfe7..a2bf150 100644 --- a/src/routes/account/api-keys/+page.svelte +++ b/src/routes/account/api-keys/+page.svelte @@ -1,11 +1,6 @@ -

API Keys

-

- Bring your own API keys for select models. Messages sent using your API keys will not count - towards your monthly limits. -

+
+

API Keys

+

+ Bring your own API keys for select models. Messages sent using your API keys will not count + towards your monthly limits. +

+
{#each allProviders as provider (provider)} diff --git a/src/routes/account/models/+page.server.ts b/src/routes/account/models/+page.server.ts new file mode 100644 index 0000000..8cfc822 --- /dev/null +++ b/src/routes/account/models/+page.server.ts @@ -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[]), + }; +} diff --git a/src/routes/account/models/+page.svelte b/src/routes/account/models/+page.svelte new file mode 100644 index 0000000..5ff21c3 --- /dev/null +++ b/src/routes/account/models/+page.svelte @@ -0,0 +1,20 @@ + + +
+

Available Models

+

+ Choose which models appear in your model selector. This won't affect existing conversations. +

+
+ +{#each data.openRouterModels as model} + +{/each} diff --git a/src/routes/account/models/model.svelte b/src/routes/account/models/model.svelte new file mode 100644 index 0000000..0459bda --- /dev/null +++ b/src/routes/account/models/model.svelte @@ -0,0 +1,49 @@ + + + + + {model.name} + {showMore ? fullDescription : shortDescription ?? fullDescription} + {#if shortDescription !== null} + + {/if} + + + + +