anyllm_proxy 0.9.3

HTTP proxy translating Anthropic Messages API to OpenAI Chat Completions
import { useMemo, useState } from 'react'
import { useModels, useAddModel, useRemoveModel, useDiscoverModels, useBackends, useManagedBackends } from '../../api/queries'
import AsyncBoundary from '../../components/shared/AsyncBoundary'
import ConfirmDialog from '../../components/shared/ConfirmDialog'

const AUTH_HINTS: Record<string, { text: string; needsKey: boolean }> = {
  openrouter: { text: 'Public, no key needed', needsKey: false },
  deepinfra: { text: 'Public, no key needed', needsKey: false },
  ollama: { text: 'No key needed (local)', needsKey: false },
  configured: { text: 'API key required', needsKey: true },
  custom: { text: 'API key may be required', needsKey: true },
}

function KeyIcon() {
  return (
    <svg width="12" height="12" viewBox="0 0 16 16" fill="none" className="key-icon-inline">
      <path
        d="M10.5 1a4.5 4.5 0 0 0-4.1 6.35L2 11.75V15h3.25v-2H7v-1.75h1.75L9.65 10.4A4.5 4.5 0 1 0 10.5 1zm1 3a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"
        fill="currentColor"
      />
    </svg>
  )
}

export default function Models() {
  const modelsQuery = useModels()
  const add = useAddModel()
  const remove = useRemoveModel()
  const discover = useDiscoverModels()
  const { data: backends } = useBackends()
  const { data: managedBackends } = useManagedBackends()
  const [name, setName] = useState('')
  const [model, setModel] = useState('')
  const [provider, setProvider] = useState('openai')
  const [backendName, setBackendName] = useState('')
  const [discoverSource, setDiscoverSource] = useState('openrouter')
  const [customUrl, setCustomUrl] = useState('')
  const [search, setSearch] = useState('')
  const [pendingRemove, setPendingRemove] = useState<string | null>(null)

  const hint = AUTH_HINTS[discoverSource] ?? AUTH_HINTS.custom

  function handleDiscover() {
    discover.mutate({
      source: discoverSource,
      ...(discoverSource === 'custom' ? { url: customUrl } : {}),
    })
  }

  function doRemove() {
    if (!pendingRemove) return Promise.resolve()
    return remove.mutateAsync(pendingRemove).then(() => undefined)
  }

  const filter = search.trim().toLowerCase()
  const filteredModels = useMemo(() => {
    const all = modelsQuery.data?.models ?? []
    if (!filter) return all
    return all.filter((m) =>
      m.name.toLowerCase().includes(filter) ||
      m.model.toLowerCase().includes(filter) ||
      m.provider.toLowerCase().includes(filter),
    )
  }, [modelsQuery.data, filter])

  return (
    <div>
      {/* Discover models section */}
      <div className="models-discover">
        <div className="section-label">Discover Models</div>
        <div className="models-discover-row">
          <select value={discoverSource} onChange={(e) => { setDiscoverSource(e.target.value); discover.reset() }}>
            <option value="openrouter">OpenRouter</option>
            <option value="deepinfra">DeepInfra</option>
            <option value="ollama">Ollama (local)</option>
            <option value="configured">Configured backend</option>
            <option value="custom">Custom URL</option>
          </select>
          {discoverSource === 'custom' && (
            <input
              name="discover-url"
              placeholder="https://api.example.com"
              value={customUrl}
              onChange={(e) => setCustomUrl(e.target.value)}
              style={{ minWidth: 220 }}
            />
          )}
          <button
            className="btn btn-secondary"
            onClick={handleDiscover}
            disabled={discover.isPending || (discoverSource === 'custom' && !customUrl)}
          >
            {discover.isPending ? 'Fetching...' : 'Fetch'}
          </button>
          <span className="dim models-discover-hint">
            {hint.needsKey && <KeyIcon />}{hint.text}
          </span>
        </div>

        {discover.isError && (
          <div className="inline-error">
            {discover.error.message}
          </div>
        )}

        {discover.data && discover.data.models.length > 0 && (
          <div className="models-discover-results">
            <div className="dim models-discover-count">
              {discover.data.models.length} model{discover.data.models.length !== 1 ? 's' : ''} found.
              Click to populate the form below.
            </div>
            <div className="models-discover-list">
              {discover.data.models.map((m) => (
                <div
                  key={m.id}
                  onClick={() => setModel(m.id)}
                  className={`models-discover-item${model === m.id ? ' is-selected' : ''}`}
                >
                  <span className="mono">{m.id}</span>
                  {m.name && m.name !== m.id && <span className="dim models-discover-item-name">{m.name}</span>}
                </div>
              ))}
            </div>
          </div>
        )}

        {discover.data && discover.data.models.length === 0 && (
          <div className="dim models-discover-count">No models returned.</div>
        )}
      </div>

      {/* Datalist for backend name suggestions */}
      <datalist id="backends-list">
        {backends?.map(b => (
          <option key={b.name} value={b.name}>{b.name}</option>
        ))}
        {managedBackends?.backends.map(b => (
          <option key={`managed-${b.name}`} value={b.name}>{b.name} (managed)</option>
        ))}
      </datalist>

      {/* Manual add model form */}
      <div className="form-group">
        <div className="form-label">Add Model</div>
        <div className="form-row" style={{ flexWrap: 'wrap' }}>
          <input name="model-name" placeholder="Virtual name" value={name} onChange={(e) => setName(e.target.value)} />
          <input name="model-id" placeholder="Model ID" value={model} onChange={(e) => setModel(e.target.value)} />
          <select name="provider" value={provider} onChange={(e) => setProvider(e.target.value)}>
            <option value="openai">openai</option>
            <option value="anthropic">anthropic</option>
            <option value="gemini">gemini</option>
            <option value="vertex">vertex</option>
            <option value="azure">azure</option>
            <option value="bedrock">bedrock</option>
          </select>
          <input
            name="backend"
            placeholder="Backend (optional)"
            value={backendName}
            onChange={(e) => setBackendName(e.target.value)}
            list="backends-list"
          />
          <button
            className="btn btn-primary"
            onClick={() => add.mutate({ name, model, provider, ...(backendName ? { backend_name: backendName } : {}) })}
            disabled={!name || !model || add.isPending}
          >
            Add
          </button>
        </div>
      </div>

      <div className="toolbar">
        <input
          type="search"
          name="models-search"
          placeholder="Search models…"
          value={search}
          onChange={(e) => setSearch(e.target.value)}
          className="toolbar-search"
        />
        {modelsQuery.data && (
          <span className="dim toolbar-count">
            {filteredModels.length} of {modelsQuery.data.models.length}
          </span>
        )}
      </div>

      <AsyncBoundary
        query={modelsQuery}
        errorTitle="Failed to load models"
        empty={{
          when: (d) => (d.models?.length ?? 0) === 0,
          render: () => (
            <div className="empty-cta">
              <div className="empty-cta-title">No models configured</div>
              <div className="empty-cta-body">
                Add a model above, or use Discover to pull a catalog from OpenRouter, DeepInfra, Ollama, or a custom endpoint.
              </div>
            </div>
          ),
        }}
      >
        {(data) =>
          filteredModels.length === 0 ? (
            <div className="empty">No models match "{search}".</div>
          ) : (
            <table className="route-table">
              <thead>
                <tr><th>Virtual Name</th><th>Model</th><th>Provider</th><th>Strategy</th><th></th></tr>
              </thead>
              <tbody>
                {filteredModels.map((m) => (
                  <tr key={`${m.name}-${m.model}`}>
                    <td className="mono">{m.name}</td>
                    <td className="mono">{m.model}</td>
                    <td className="dim">{m.provider}</td>
                    <td className="dim">{data.routing_strategy}</td>
                    <td>
                      <button className="btn btn-danger btn-sm" onClick={() => setPendingRemove(m.name)}>Remove</button>
                    </td>
                  </tr>
                ))}
              </tbody>
            </table>
          )
        }
      </AsyncBoundary>

      <ConfirmDialog
        open={pendingRemove !== null}
        onClose={() => setPendingRemove(null)}
        onConfirm={doRemove}
        title="Remove model?"
        message={
          <>
            Remove model <span className="mono">{pendingRemove}</span>? Requests using this virtual name
            will fail until another model with the same name is added.
          </>
        }
        confirmLabel="Remove"
      />
    </div>
  )
}