Add features & update project
Build and Push Spoon Images / quality (push) Successful in 1m41s
Build and Push Spoon Images / build-images (push) Successful in 7m4s

This commit is contained in:
Gabriel Brown
2026-06-23 02:06:58 -04:00
parent fe72fc2957
commit d207b8b0b8
26 changed files with 1257 additions and 231 deletions
@@ -1,8 +1,12 @@
'use client';
import type { ProviderModelOption } from '@/lib/models-dev';
import { useEffect, useMemo, useState } from 'react';
import { loadModelsDevOptions } from '@/lib/models-dev';
import type { ProviderModelOption } from '@/lib/provider-model-options';
import { useMemo, useState } from 'react';
import {
modelOptionsFromIds,
suggestedModelOptions,
supportsCustomModelOptions,
} from '@/lib/provider-model-options';
import { useAction, useMutation, useQuery } from 'convex/react';
import { makeFunctionReference } from 'convex/server';
import { KeyRound, Trash2 } from 'lucide-react';
@@ -11,6 +15,7 @@ import { toast } from 'sonner';
import type { Id } from '@spoon/backend/convex/_generated/dataModel.js';
import { api } from '@spoon/backend/convex/_generated/api.js';
import {
Badge,
Button,
Card,
CardContent,
@@ -50,6 +55,7 @@ const saveProfileRef = makeFunctionReference<
secret?: string;
baseUrl?: string;
defaultModel: string;
modelOptions?: string[];
reasoningEffort: ReasoningEffort;
enabled: boolean;
},
@@ -119,33 +125,24 @@ export const AiProviderProfilesPanel = () => {
);
const [secret, setSecret] = useState('');
const [baseUrl, setBaseUrl] = useState('');
const [defaultModelValue, setDefaultModelValue] = useState('');
const [modelOptions, setModelOptions] = useState<ProviderModelOption[]>([]);
const [defaultModelValue, setDefaultModelValue] = useState(
suggestedModelOptions('openai')[0]?.id ?? '',
);
const [modelOptions, setModelOptions] = useState<ProviderModelOption[]>(
suggestedModelOptions('openai'),
);
const [customModelId, setCustomModelId] = useState('');
const [reasoningEffort, setReasoningEffort] =
useState<ReasoningEffort>('medium');
const [enabled, setEnabled] = useState(true);
const [saving, setSaving] = useState(false);
useEffect(() => {
let cancelled = false;
loadModelsDevOptions(provider)
.then((options) => {
if (cancelled) return;
setModelOptions(options);
setDefaultModelValue((current) =>
current && options.some((option) => option.id === current)
? current
: (options[0]?.id ?? ''),
);
})
.catch((error: unknown) => {
console.error(error);
if (!cancelled) setModelOptions([]);
});
return () => {
cancelled = true;
};
}, [provider]);
const resetModelOptions = (nextProvider: Provider) => {
const options = suggestedModelOptions(nextProvider);
setModelOptions(options);
setDefaultModelValue(options[0]?.id ?? '');
setCustomModelId('');
};
const reset = () => {
setProfileId(undefined);
@@ -153,6 +150,8 @@ export const AiProviderProfilesPanel = () => {
setSecret('');
setBaseUrl('');
setDefaultModelValue('');
setModelOptions(suggestedModelOptions('openai'));
setCustomModelId('');
setReasoningEffort('medium');
setEnabled(true);
setName('OpenAI');
@@ -165,6 +164,14 @@ export const AiProviderProfilesPanel = () => {
setSecret('');
setBaseUrl(profile.baseUrl ?? '');
setDefaultModelValue(profile.defaultModel);
setModelOptions(
modelOptionsFromIds(
profile.modelOptions?.length
? profile.modelOptions
: [profile.defaultModel],
),
);
setCustomModelId('');
setReasoningEffort(profile.reasoningEffort as ReasoningEffort);
setEnabled(profile.enabled);
};
@@ -181,6 +188,7 @@ export const AiProviderProfilesPanel = () => {
secret: secret.trim() ? secret : undefined,
baseUrl: baseUrl.trim() || undefined,
defaultModel: defaultModelValue,
modelOptions: modelOptions.map((model) => model.id),
reasoningEffort,
enabled,
});
@@ -310,6 +318,7 @@ export const AiProviderProfilesPanel = () => {
onValueChange={(value) => {
const nextProvider = value as Provider;
setProvider(nextProvider);
resetModelOptions(nextProvider);
setName(
providerOptions
.find((option) => option.value === nextProvider)
@@ -397,9 +406,47 @@ export const AiProviderProfilesPanel = () => {
</SelectContent>
</Select>
<p className='text-muted-foreground text-xs'>
Models are loaded from Models.dev, the catalog OpenCode uses
for provider/model metadata.
Saved model options are used by Spoons. Add custom model IDs
for compatible provider gateways.
</p>
<div className='rounded-md border p-2'>
<p className='text-muted-foreground mb-2 text-xs'>
Available model options
</p>
<div className='flex flex-wrap gap-2'>
{modelOptions.map((model) => (
<Badge key={model.id} variant='outline'>
{model.id}
</Badge>
))}
</div>
</div>
{supportsCustomModelOptions(provider) ? (
<div className='flex gap-2'>
<Input
value={customModelId}
placeholder='provider/model-id'
onChange={(event) => setCustomModelId(event.target.value)}
/>
<Button
type='button'
variant='outline'
onClick={() => {
const id = customModelId.trim();
if (!id) return;
setModelOptions((current) =>
current.some((model) => model.id === id)
? current
: [...current, ...modelOptionsFromIds([id])],
);
setDefaultModelValue((current) => current || id);
setCustomModelId('');
}}
>
Add
</Button>
</div>
) : null}
</div>
<div className='grid gap-2'>
<Label>Thinking</Label>