anyllm_proxy 0.9.6

HTTP proxy translating Anthropic Messages API to OpenAI Chat Completions
Documentation
import { useState, useEffect } from 'react'
import {
  useCatalogProviders,
  useCreateManagedBackend,
  useUpdateManagedBackend,
} from '../../api/queries'
import type { ManagedBackend, CatalogProvider } from '../../api/types'
import { getProviderFields } from '../../utils/providerFields'
import ProviderIcon from '../../components/shared/ProviderIcon'

interface BackendFormProps {
  initial?: ManagedBackend | null
  onSuccess: () => void
  onCancel: () => void
}

export function BackendForm({ initial, onSuccess, onCancel }: BackendFormProps) {
  const isEdit = !!initial
  const { data: providers = [] } = useCatalogProviders()
  const create = useCreateManagedBackend()
  const update = useUpdateManagedBackend()

  const [name, setName] = useState(initial?.name ?? '')
  const [providerId, setProviderId] = useState(initial?.provider_id ?? '')
  const [fields, setFields] = useState<Record<string, string>>({})
  const [submitError, setSubmitError] = useState<string | null>(null)

  useEffect(() => {
    if (providers.length > 0 && !providerId) {
      setProviderId(initial?.provider_id ?? providers[0].id)
    }
  }, [providers.length]) // eslint-disable-line react-hooks/exhaustive-deps

  const selectedProvider: CatalogProvider | undefined = providers.find(p => p.id === providerId)
    ?? (providers.length > 0 ? providers[0] : undefined)

  const fieldDefs = selectedProvider ? getProviderFields(selectedProvider) : []

  const authFields = fieldDefs.filter(f => f.group === 'auth')
  const endpointFields = fieldDefs.filter(f => f.group === 'endpoint')
  const limitFields = fieldDefs.filter(f => f.group === 'limits')

  function getFieldValue(fieldName: string): string {
    return fields[fieldName] ?? ''
  }

  function setFieldValue(fieldName: string, value: string) {
    setFields(prev => ({ ...prev, [fieldName]: value }))
  }

  function isCredSetForField(fieldName: string): boolean {
    if (!isEdit || !initial) return false
    if (fieldName === 'api_key') return initial.api_key_set
    if (fieldName === 'aws_secret_access_key' || fieldName === 'aws_access_key_id') return initial.aws_creds_set
    return false
  }

  function omitEmpty(obj: Record<string, string>): Record<string, string> {
    return Object.fromEntries(Object.entries(obj).filter(([, v]) => v !== ''))
  }

  function handleSubmit(e: React.FormEvent) {
    e.preventDefault()
    setSubmitError(null)

    const cleanedFields = omitEmpty(fields)

    if (isEdit && initial) {
      update.mutate(
        { name: initial.name, data: cleanedFields },
        {
          onSuccess: () => onSuccess(),
          onError: (err) => setSubmitError(err.message),
        },
      )
    } else {
      if (!name) {
        setSubmitError('Name is required')
        return
      }
      create.mutate(
        { name, provider_id: providerId, ...cleanedFields } as Parameters<typeof create.mutate>[0],
        {
          onSuccess: () => onSuccess(),
          onError: (err) => setSubmitError(err.message),
        },
      )
    }
  }

  const isPending = create.isPending || update.isPending

  function renderField(fd: ReturnType<typeof getProviderFields>[0]) {
    const val = getFieldValue(fd.name)
    const credSet = isCredSetForField(fd.name)
    const inputType = fd.type === 'url' ? 'text' : fd.type

    return (
      <div key={fd.name} style={{ marginBottom: 10 }}>
        <div style={{ fontSize: 12, color: 'var(--text-2)', marginBottom: 3 }}>
          {fd.label}{fd.required && <span style={{ color: 'var(--err)', marginLeft: 2 }}>*</span>}
        </div>
        <input
          type={inputType}
          value={val}
          placeholder={credSet ? '••••••••' : fd.placeholder}
          onChange={(e) => setFieldValue(fd.name, e.target.value)}
          style={{ width: '100%' }}
        />
        {fd.hint && (
          <div style={{ fontSize: 11, color: 'var(--text-2)', marginTop: 3 }}>{fd.hint}</div>
        )}
      </div>
    )
  }

  return (
    <form onSubmit={handleSubmit} style={{ padding: '12px', background: 'var(--bg-raised)', border: '1px solid var(--border)', borderRadius: 'var(--rm)', marginBottom: 14 }}>
      <div style={{ fontWeight: 600, marginBottom: 12, fontSize: 13 }}>
        {isEdit ? `Edit backend: ${initial!.name}` : 'Add managed backend'}
      </div>

      {/* Provider selector */}
      <div style={{ marginBottom: 10 }}>
        <div style={{ fontSize: 12, color: 'var(--text-2)', marginBottom: 3 }}>
          Provider<span style={{ color: 'var(--err)', marginLeft: 2 }}>*</span>
        </div>
        <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
          {providerId && <ProviderIcon id={providerId} size={18} style={{ flexShrink: 0 }} />}
          <select
            value={providerId}
            onChange={(e) => { setProviderId(e.target.value); setFields({}) }}
            disabled={isEdit}
            style={{ flex: 1 }}
          >
          {providers.length === 0 && (
            <option value="">Loading providers…</option>
          )}
          {(['implemented', 'wired', 'stub'] as const).map(status => {
            const group = providers.filter(p => p.status === status)
            if (group.length === 0) return null
            return (
              <optgroup key={status} label={status.charAt(0).toUpperCase() + status.slice(1)}>
                {group.map(p => (
                  <option key={p.id} value={p.id}>{p.display_name}</option>
                ))}
              </optgroup>
            )
          })}
          </select>
        </div>
      </div>

      <div style={{ marginBottom: 10 }}>
        <div style={{ fontSize: 12, color: 'var(--text-2)', marginBottom: 3 }}>
          Name<span style={{ color: 'var(--err)', marginLeft: 2 }}>*</span>
        </div>
        <input
          type="text"
          value={name}
          onChange={(e) => setName(e.target.value)}
          required
          pattern="[a-zA-Z0-9_\-]+"
          placeholder="my-backend"
          disabled={isEdit}
          style={{ width: '100%' }}
        />
        {!isEdit && (
          <div style={{ fontSize: 11, color: 'var(--text-2)', marginTop: 3 }}>Letters, numbers, underscores, hyphens only</div>
        )}
      </div>

      {authFields.length > 0 && (
        <div style={{ marginBottom: 4 }}>
          <div className="section-label" style={{ marginBottom: 6 }}>Authentication</div>
          {authFields.map(renderField)}
        </div>
      )}

      {endpointFields.length > 0 && (
        <div style={{ marginBottom: 4 }}>
          <div className="section-label" style={{ marginBottom: 6 }}>Endpoint</div>
          {endpointFields.map(renderField)}
        </div>
      )}

      {limitFields.length > 0 && (
        <details style={{ marginBottom: 10 }}>
          <summary style={{ fontSize: 11, color: 'var(--text-2)', cursor: 'pointer', textTransform: 'uppercase', letterSpacing: '0.07em', fontWeight: 500, marginBottom: 6 }}>
            Rate Limits
          </summary>
          <div style={{ marginTop: 8 }}>
            {limitFields.map(renderField)}
          </div>
        </details>
      )}

      {/* Error */}
      {submitError && (
        <div style={{ marginBottom: 10, padding: '6px 10px', background: 'var(--err-dim)', borderLeft: '3px solid var(--err)', borderRadius: 'var(--r)', fontSize: 12 }}>
          {submitError}
        </div>
      )}

      {/* Actions */}
      <div style={{ display: 'flex', gap: 8 }}>
        <button type="submit" className="btn btn-primary btn-sm" disabled={isPending}>
          {isPending ? 'Saving…' : isEdit ? 'Save changes' : 'Create backend'}
        </button>
        <button type="button" className="btn btn-secondary btn-sm" onClick={onCancel} disabled={isPending}>
          Cancel
        </button>
      </div>
    </form>
  )
}