feat: add model and key configuration options

Add UI components for configuring the model kind (OpenAI or
Anthropic), key option (Butler API or bring your own key),
and associated settings.

The model kind can be set to either OpenAI or Anthropic. The
key option allows using the Butler API proxy or providing
your own API key.

When using your own key, the API key and model version can
be specified for the selected model kind (OpenAI or
Anthropic).

The selected options are persisted and loaded from the
backend settings.
This commit is contained in:
Caleb Owens 2024-03-09 01:03:10 +00:00 committed by Mattias Granlund
parent 15956c0729
commit 12aefe73a3
4 changed files with 134 additions and 36 deletions

View File

@ -1,8 +1,8 @@
import { getAnthropicKey, getAnthropicModel, getModelKind, getOpenAIKey, getOpenAIModel, getTokenOption, KeyOption, ModelKind } from './summarizer_settings'; import { getAnthropicKey, getAnthropicModel, getModelKind, getOpenAIKey, getOpenAIModel, getKeyOption, KeyOption, ModelKind } from './summarizer_settings';
import { import {
type AIProvider, type AIProvider,
ButlerAIProvider, ButlerAIProvider,
OpenAIProvider as AnthropicAIProvider, AnthropicAIProvider,
OpenAIProvider OpenAIProvider
} from '$lib/backend/aiProviders'; } from '$lib/backend/aiProviders';
import { getCloudApiClient, type User } from '$lib/backend/cloud'; import { getCloudApiClient, type User } from '$lib/backend/cloud';
@ -99,9 +99,9 @@ interface SummarizerContext {
// Secondly, if the user has opted to bring their own key but hasn't provided one, it will return undefined // Secondly, if the user has opted to bring their own key but hasn't provided one, it will return undefined
export async function buildSummarizer(context: SummarizerContext): Promise<Summarizer | undefined> { export async function buildSummarizer(context: SummarizerContext): Promise<Summarizer | undefined> {
const modelKind = await getModelKind(); const modelKind = await getModelKind();
const tokenOption = await getTokenOption(); const keyOption = await getKeyOption();
if (tokenOption === KeyOption.ButlerAPI) { if (keyOption === KeyOption.ButlerAPI) {
if (!context.user) return; if (!context.user) return;
const aiProvider = new ButlerAIProvider(getCloudApiClient(), context.user, modelKind); const aiProvider = new ButlerAIProvider(getCloudApiClient(), context.user, modelKind);

View File

@ -42,12 +42,12 @@ export function setModelKind(modelKind: ModelKind) {
const tokenOptionConfigKey = 'tokenOption'; const tokenOptionConfigKey = 'tokenOption';
export async function getTokenOption(): Promise<KeyOption> { export async function getKeyOption(): Promise<KeyOption> {
const tokenKind = (await gitGetConfig(tokenOptionConfigKey)) as KeyOption | undefined; const tokenKind = (await gitGetConfig(tokenOptionConfigKey)) as KeyOption | undefined;
return tokenKind || KeyOption.ButlerAPI; return tokenKind || KeyOption.ButlerAPI;
} }
export function setTokenOption(tokenOption: KeyOption) { export function setKeyOption(tokenOption: KeyOption) {
return gitSetConfig(tokenOptionConfigKey, tokenOption); return gitSetConfig(tokenOptionConfigKey, tokenOption);
} }
@ -80,7 +80,7 @@ export async function getAnthropicKey(): Promise<string | undefined> {
return key || undefined; return key || undefined;
} }
export function setAnthropicToken(token: string) { export function setAnthropicKey(token: string) {
return gitSetConfig(anthropicKeyConfigKey, token); return gitSetConfig(anthropicKeyConfigKey, token);
} }

View File

@ -0,0 +1,125 @@
<script lang="ts">
import SectionCard from "$lib/components/SectionCard.svelte";
import { KeyOption, ModelKind, getModelKind, getKeyOption, setModelKind, setKeyOption, getAnthropicKey, setAnthropicKey, setOpenAIKey, getOpenAIKey, AnthropicModel, getAnthropicModel, OpenAIModel, getOpenAIModel, setAnthropicModel, setOpenAIModel } from "$lib/backend/summarizer_settings";
import Select from "./Select.svelte";
import SelectItem from "./SelectItem.svelte";
import TextBox from "./TextBox.svelte";
let modelKind: { name: string, value: ModelKind } | undefined;
getModelKind().then((kind) => modelKind = modelKinds.find((option) => option.value == kind))
$: if (modelKind) setModelKind(modelKind.value)
const modelKinds = [
{
name: "Open AI",
value: ModelKind.OpenAI
},
{
name: "Anthropic",
value: ModelKind.Anthropic
}
]
let keyOption: { name: string, value: KeyOption } | undefined;
getKeyOption().then((persistedKeyOption) => keyOption = keyOptions.find((option) => option.value == persistedKeyOption))
$: if (keyOption) setKeyOption(keyOption.value)
const keyOptions = [
{
name: "Butler API",
value: KeyOption.ButlerAPI
},
{
name: "Bring your own key",
value: KeyOption.BringYourOwn
}
]
let openAIKey: string | undefined;
getOpenAIKey().then((persistedOpenAIKey) => openAIKey = persistedOpenAIKey)
$: if (openAIKey) setOpenAIKey(openAIKey)
let openAIModel: { name: string, value: OpenAIModel } | undefined;
getOpenAIModel().then((persistedOpenAIModel) => openAIModel = openAIModelOptions.find((option) => option.value == persistedOpenAIModel))
$: if (openAIModel) setOpenAIModel(openAIModel.value)
const openAIModelOptions = [
{
name: "GPT 3.5 Turbo",
value: OpenAIModel.GPT35Turbo
},
{
name: "GPT 4",
value: OpenAIModel.GPT4
},
{
name: "GPT 4 Turbo",
value: OpenAIModel.GPT4Turbo
},
]
let anthropicKey: string | undefined;
getAnthropicKey().then((persistedAnthropicKey) => anthropicKey = persistedAnthropicKey)
$: if (anthropicKey) setAnthropicKey(anthropicKey)
let anthropicModel: { name: string, value: AnthropicModel } | undefined;
getAnthropicModel().then((persistedAnthropicModel) => anthropicModel = anthropicModelOptions.find((option) => option.value == persistedAnthropicModel))
$: if (anthropicModel) setAnthropicModel(anthropicModel.value)
const anthropicModelOptions = [
{
name: "Sonnet",
value: AnthropicModel.Sonnet
},
{
name: "Opus",
value: AnthropicModel.Opus
}
]
</script>
<SectionCard>
<svelte:fragment slot="title">Model Kind</svelte:fragment>
<svelte:fragment slot="body">
GitButler supports OpenAI and Anthropic for various summerization tasks, either proxied via the GitButler servers or in a bring your own key configuration.
</svelte:fragment>
<Select items={modelKinds} bind:value={modelKind} itemId="value" labelId="name">
<SelectItem slot="template" let:item>
{item.name}
</SelectItem>
</Select>
</SectionCard>
<SectionCard>
<svelte:fragment slot="title">Key Configuration</svelte:fragment>
<svelte:fragment slot="body">
GitButler can either be configured to be proxied via the GitButler servers or to use your own key.
</svelte:fragment>
<Select items={keyOptions} bind:value={keyOption} itemId="value" labelId="name">
<SelectItem slot="template" let:item>
{item.name}
</SelectItem>
</Select>
{#if keyOption?.value === KeyOption.BringYourOwn}
{#if modelKind?.value == ModelKind.Anthropic}
<TextBox label="Anthropic API Key" bind:value={anthropicKey}/>
<Select items={anthropicModelOptions} bind:value={anthropicModel} itemId="value" labelId="name" label="Model Version">
<SelectItem slot="template" let:item>
{item.name}
</SelectItem>
</Select>
{:else if modelKind?.value == ModelKind.OpenAI}
<TextBox label="OpenAI API Key" bind:value={openAIKey}/>
<Select items={openAIModelOptions} bind:value={openAIModel} itemId="value" labelId="name" label="Model Version">
<SelectItem slot="template" let:item>
{item.name}
</SelectItem>
</Select>
{/if}
{/if}
</SectionCard>

View File

@ -25,6 +25,7 @@
import { getContext } from 'svelte'; import { getContext } from 'svelte';
import type { PageData } from './$types'; import type { PageData } from './$types';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import AiSettings from '$lib/components/AISettings.svelte';
const userSettings = getContext(SETTINGS_CONTEXT) as SettingsStore; const userSettings = getContext(SETTINGS_CONTEXT) as SettingsStore;
@ -335,35 +336,7 @@
</ContentWrapper> </ContentWrapper>
{:else if currentSection === 'ai'} {:else if currentSection === 'ai'}
<ContentWrapper title="AI Options"> <ContentWrapper title="AI Options">
<SectionCard> <AiSettings />
<svelte:fragment slot="title">AI Provider</svelte:fragment>
<svelte:fragment slot="caption">
GitButler uses a a connection to its API to provide AI functionality, but supports
additional providers
</svelte:fragment>
<TextBox readonly selectall bind:value={sshKey} />
<div class="row-buttons">
<Button
kind="filled"
color="primary"
icon="copy"
on:click={() => copyToClipboard(sshKey)}
>
Copy to Clipboard
</Button>
<Button
kind="outlined"
color="neutral"
icon="open-link"
on:mousedown={() => {
openExternalUrl('https://github.com/settings/ssh/new');
}}
>
Add key to GitHub
</Button>
</div>
</SectionCard>
</ContentWrapper> </ContentWrapper>
{/if} {/if}
</section> </section>