File size: 4,801 Bytes
9ab40fd
1778c9e
 
1979653
1778c9e
7357f85
 
c6f83f5
1778c9e
64cfbce
812d95a
9ab40fd
936176c
1778c9e
936176c
 
 
1778c9e
9ab40fd
b924465
1778c9e
357ab93
1778c9e
 
 
 
 
9ab40fd
 
936176c
 
 
 
9ab40fd
c6f83f5
1778c9e
c6f83f5
1778c9e
c6f83f5
936176c
9ab40fd
 
 
 
 
 
 
 
 
 
 
 
b34bca6
9ab40fd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bf8e775
 
 
4fca6da
bf8e775
1979653
 
 
 
 
 
 
9ab40fd
 
 
 
 
 
 
 
 
 
c6f83f5
ba9894c
 
 
7e80e42
ba9894c
9ab40fd
1979653
1778c9e
1979653
 
 
 
 
 
 
 
9ab40fd
64cfbce
 
 
 
 
9ab40fd
 
c6f83f5
bf8e775
1979653
84f775e
9ab40fd
 
 
 
1979653
 
 
 
 
 
 
 
 
 
9ab40fd
84f775e
e85b5fc
 
 
9ab40fd
bf8e775
9ab40fd
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
<script lang="ts">
	import type { ConversationClass } from "$lib/state/conversations.svelte";
	import { models } from "$lib/state/models.svelte";
	import { pricing } from "$lib/state/pricing.svelte";
	import type { Model } from "$lib/types.js";
	import { randomPick } from "$lib/utils/array.js";
	import { cn } from "$lib/utils/cn.js";
	import { Select } from "melt/builders";
	import { run } from "svelte/legacy";
	import IconCaret from "~icons/carbon/chevron-down";
	import IconProvider from "../icon-provider.svelte";

	interface Props {
		conversation: ConversationClass & { model: Model };
		class?: string | undefined;
	}

	const { conversation, class: classes = undefined }: Props = $props();

	function reset(providers: typeof conversation.model.inferenceProviderMapping) {
		const validProvider = providers.find(p => p.provider === conversation.data.provider);
		if (validProvider || conversation.data.provider === "auto") return;
		if (providers) {
			conversation.update({ provider: randomPick(providers)?.provider });
		} else {
			conversation.update({ modelId: randomPick(models.all)?.id });
		}
	}

	let providers = $derived(conversation.model.inferenceProviderMapping);
	run(() => {
		reset(providers);
	});

	const select = new Select<string, false>({
		value: () => conversation.data.provider,
		onValueChange(v) {
			conversation.update({ provider: v });
		},
	});

	const nameMap: Record<string, string> = {
		"sambanova": "SambaNova",
		"fal": "fal",
		"cerebras": "Cerebras",
		"replicate": "Replicate",
		"black-forest-labs": "Black Forest Labs",
		"fireworks-ai": "Fireworks",
		"together": "Together AI",
		"nebius": "Nebius AI Studio",
		"hyperbolic": "Hyperbolic",
		"novita": "Novita",
		"cohere": "Cohere",
		"hf-inference": "HF Inference API",
	};
	const UPPERCASE_WORDS = ["hf", "ai"];

	function formatName(provider: string) {
		if (provider in nameMap) return nameMap[provider];

		const words = provider
			.toLowerCase()
			.split("-")
			.map(word => {
				if (UPPERCASE_WORDS.includes(word)) {
					return word.toUpperCase();
				} else {
					return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
				}
			});

		return words.join(" ");
	}

	function getProviderName(provider: string) {
		if (provider in nameMap) return formatName(provider);
		return provider === "auto" ? "Auto" : provider;
	}

	function getProviderPricing(provider: string) {
		if (provider === "auto") return null;
		const pd = pricing.getPricing(conversation.model.id, provider);
		return pricing.formatPricing(pd);
	}
	const providerPricing = $derived(getProviderPricing(conversation.data.provider ?? ""));
</script>

<div class="flex flex-col gap-2">
	<!--
	<label class="flex items-baseline gap-2 text-sm font-medium text-gray-900 dark:text-white">
		Providers<span class="text-xs font-normal text-gray-400"></span>
	</label>
	-->

	<button
		{...select.trigger}
		class={cn(
			"relative flex items-center justify-between gap-6 overflow-hidden rounded-lg border bg-gray-100/80 px-3 py-1.5 leading-tight whitespace-nowrap shadow-sm",
			"hover:brightness-95 dark:border-gray-700 dark:bg-gray-800 dark:hover:brightness-110",
			classes,
		)}
	>
		<div class="flex items-center gap-2 text-sm">
			<IconProvider provider={conversation.data.provider} />
			<div class="flex flex-col items-start">
				<span>{getProviderName(conversation.data.provider ?? "") ?? "loading"}</span>
				{#if providerPricing}
					<span class="text-xs text-gray-500 dark:text-gray-400">
						In: {providerPricing.input} • Out: {providerPricing.output}
					</span>
				{/if}
			</div>
		</div>
		<div
			class="absolute right-2 grid size-4 flex-none place-items-center rounded-sm bg-gray-100 text-xs dark:bg-gray-600"
		>
			<IconCaret />
		</div>
	</button>

	<div {...select.content} class="rounded-lg border bg-gray-100 dark:border-gray-700 dark:bg-gray-800">
		{#snippet option(provider: string)}
			{@const providerPricing = getProviderPricing(provider)}
			<div {...select.getOption(provider)} class="group block w-full p-1 text-sm dark:text-white">
				<div
					class="flex items-center gap-2 rounded-md px-2 py-1.5 group-data-[highlighted]:bg-gray-200 dark:group-data-[highlighted]:bg-gray-700"
				>
					<IconProvider {provider} />
					<div class="flex flex-col">
						<span>{getProviderName(provider)}</span>
						{#if providerPricing}
							<div class="flex flex-col">
								<span class="text-xs text-gray-500 dark:text-gray-400">
									In: {providerPricing.input} • Out: {providerPricing.output}
								</span>
							</div>
						{/if}
					</div>
				</div>
			</div>
		{/snippet}
		{#each conversation.model.inferenceProviderMapping as { provider, providerId } (provider + providerId)}
			{@render option(provider)}
		{/each}
		{@render option("auto")}
	</div>
</div>