anyllm_proxy 0.9.6

HTTP proxy translating Anthropic Messages API to OpenAI Chat Completions
Documentation
import { Fragment, useRef, useState } from 'react'
import {
  useConfig, useSaveConfig, useDeleteConfigOverride, useEnv,
  useImportEnv, downloadEnvExport,
} from '../../api/queries'
import EmptyState from '../../components/shared/EmptyState'
import ConfirmDialog from '../../components/shared/ConfirmDialog'
import type { EnvImportResponse, EnvImportError } from '../../api/types'
import { ManagedBackendsSection } from './ManagedBackendsSection'

const RESTART_KEY = 'env_import_pending_restart'

function restartPending() {
  return sessionStorage.getItem(RESTART_KEY) === '1'
}

export default function Settings({ configured = true }: { configured?: boolean }) {
  const { data: cfg, isLoading, error } = useConfig()
  const { data: envData } = useEnv()
  const save = useSaveConfig()
  const del = useDeleteConfigOverride()
  const importEnv = useImportEnv()
  const fileRef = useRef<HTMLInputElement>(null)

  const [form, setForm] = useState<Record<string, string>>({})
  const [importResult, setImportResult] = useState<EnvImportResponse | null>(null)
  const [importError, setImportError] = useState<EnvImportError | null>(null)
  const [exportError, setExportError] = useState<string | null>(null)
  const [showRestartBanner, setShowRestartBanner] = useState(restartPending)
  const [pendingReset, setPendingReset] = useState<string | null>(null)

  function doReset() {
    if (!pendingReset) return Promise.resolve()
    const key = pendingReset
    return del.mutateAsync(key).then(() => undefined)
  }

  function handleSave(key: string, currentValue: string) {
    save.mutate({ [key]: form[key] ?? currentValue })
  }

  function handleFileChange(e: React.ChangeEvent<HTMLInputElement>) {
    const file = e.target.files?.[0]
    if (!file) return
    setImportResult(null)
    setImportError(null)

    importEnv.mutate(file, {
      onSuccess(data) {
        setImportResult(data)
        sessionStorage.setItem(RESTART_KEY, '1')
        setShowRestartBanner(true)
      },
      onError(err) {
        // Try to parse hard_errors from the response body
        try {
          const parsed = JSON.parse(err.message) as EnvImportError
          if (parsed.hard_errors) {
            setImportError(parsed)
            return
          }
        } catch {
          // fall through to generic error
        }
        setImportError({ hard_errors: [err.message], warnings: [] })
      },
    })

    // Reset file input so the same file can be re-selected after fixing issues
    if (fileRef.current) fileRef.current.value = ''
  }

  async function handleExport() {
    setExportError(null)
    try {
      await downloadEnvExport()
    } catch (err) {
      setExportError(err instanceof Error ? err.message : String(err))
    }
  }

  function dismissRestartBanner() {
    sessionStorage.removeItem(RESTART_KEY)
    setShowRestartBanner(false)
  }

  return (
    <div>
      {/* Managed backends — always shown first */}
      <ManagedBackendsSection />

      {/* Getting-started notice — shown when no backend is configured */}
      {!configured && (
        <div style={{ marginBottom: 20, padding: '12px 16px', border: '1px solid var(--border)', borderLeft: '3px solid var(--warn)', borderRadius: 'var(--r)', fontSize: 13 }}>
          <div style={{ fontWeight: 600, marginBottom: 8 }}>No proxy configuration found — nothing to forward requests to.</div>
          <div style={{ marginBottom: 10 }}>
            The proxy needs a backend endpoint (where to forward) and a listen port (where to accept).
            LISTEN_PORT defaults to 3000. Create a <span className="mono">.anyllm.env</span> and import it below,
            or pass it at startup: <span className="mono">anyllm-proxy --webui --env-file .anyllm.env</span>
          </div>
          <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 10 }}>
            <div>
              <div style={{ fontWeight: 600, marginBottom: 4, fontSize: 12 }}>OpenAI</div>
              <pre style={{ margin: 0, padding: '6px 10px', background: 'var(--surface-2)', borderRadius: 'var(--r)', fontSize: 11, overflowX: 'auto' }}>
{`OPENAI_API_KEY=sk-...
PROXY_API_KEYS=my-key`}
              </pre>
            </div>
            <div>
              <div style={{ fontWeight: 600, marginBottom: 4, fontSize: 12 }}>Ollama / local LLM</div>
              <pre style={{ margin: 0, padding: '6px 10px', background: 'var(--surface-2)', borderRadius: 'var(--r)', fontSize: 11, overflowX: 'auto' }}>
{`OPENAI_BASE_URL=http://localhost:11434/v1
PROXY_OPEN_RELAY=true`}
              </pre>
            </div>
            <div>
              <div style={{ fontWeight: 600, marginBottom: 4, fontSize: 12 }}>OpenRouter / custom</div>
              <pre style={{ margin: 0, padding: '6px 10px', background: 'var(--surface-2)', borderRadius: 'var(--r)', fontSize: 11, overflowX: 'auto' }}>
{`OPENAI_BASE_URL=https://openrouter.ai/api/v1
OPENAI_API_KEY=sk-or-...
PROXY_API_KEYS=my-key`}
              </pre>
            </div>
          </div>
        </div>
      )}

      {/* Restart-required banner — shown after a successful import */}
      {showRestartBanner && (
        <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16, padding: '8px 12px', background: 'var(--warn-dim)', borderLeft: '3px solid var(--warn)', borderRadius: 'var(--r)', fontSize: 13 }}>
          <span>Restart the proxy for imported env vars to take effect.</span>
          <button className="btn btn-secondary btn-sm" onClick={dismissRestartBanner}>Dismiss</button>
        </div>
      )}

      {/* Env file import / export */}
      <div style={{ marginBottom: 24 }}>
        <div className="section-label" style={{ marginBottom: 8 }}>Env File</div>
        <div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
          <input
            ref={fileRef}
            type="file"
            accept=".env,.anyllm.env,text/plain"
            style={{ display: 'none' }}
            onChange={handleFileChange}
          />
          <button
            className="btn btn-secondary btn-sm"
            onClick={() => fileRef.current?.click()}
            disabled={importEnv.isPending}
          >
            {importEnv.isPending ? 'Importing…' : 'Import .anyllm.env'}
          </button>
          <button className="btn btn-secondary btn-sm" onClick={handleExport}>
            Export .anyllm.env
          </button>
        </div>

        {/* Import success */}
        {importResult && (
          <div style={{ marginTop: 10 }}>
            <div className="dim" style={{ marginBottom: 4 }}>
              {importResult.applied} variable{importResult.applied !== 1 ? 's' : ''} imported.
              {importResult.warnings.length === 0 && ' No issues.'}
            </div>
            {importResult.warnings.length > 0 && (
              <div style={{ marginTop: 8, padding: '8px 12px', background: 'var(--warn-dim)', borderLeft: '3px solid var(--warn)', borderRadius: 'var(--r)', fontSize: 12 }}>
                <div style={{ fontWeight: 600, marginBottom: 4 }}>Warnings</div>
                {importResult.warnings.map((w, i) => (
                  <div key={i} className="mono" style={{ fontSize: 12 }}>
                    {w.line != null && <span className="dim">[line {w.line}] </span>}
                    {w.key && <span>{w.key}: </span>}
                    {w.message}
                  </div>
                ))}
              </div>
            )}
          </div>
        )}

        {/* Import hard error */}
        {importError && (
          <div style={{ marginTop: 10, padding: '8px 12px', background: 'var(--err-dim)', borderLeft: '3px solid var(--err)', borderRadius: 'var(--r)', fontSize: 12 }}>
            <div style={{ fontWeight: 600, marginBottom: 4 }}>Import rejected</div>
            {importError.hard_errors.map((e, i) => (
              <div key={i} className="mono" style={{ fontSize: 12 }}>{e}</div>
            ))}
            {importError.warnings.length > 0 && (
              <>
                <div style={{ fontWeight: 600, marginTop: 8, marginBottom: 4 }}>Warnings (from partial parse)</div>
                {importError.warnings.map((w, i) => (
                  <div key={i} className="mono" style={{ fontSize: 12 }}>
                    {w.line != null && <span className="dim">[line {w.line}] </span>}
                    {w.message}
                  </div>
                ))}
              </>
            )}
          </div>
        )}

        {/* Export error */}
        {exportError && (
          <div style={{ marginTop: 10, padding: '8px 12px', background: 'var(--err-dim)', borderLeft: '3px solid var(--err)', borderRadius: 'var(--r)', fontSize: 12 }}>
            Export failed: {exportError}
          </div>
        )}
      </div>

      <EmptyState loading={isLoading} error={error?.message} />
      {cfg && (
        <div>
          {cfg.entries.map((entry) => {
            const inputId = `cfg-${entry.key}`
            return (
              <div className="form-group" key={entry.key}>
                <label className="form-label" htmlFor={inputId}>{entry.key}</label>
                <div className="form-row">
                  <input
                    id={inputId}
                    name={entry.key}
                    value={form[entry.key] ?? entry.value}
                    onChange={(e) => setForm((f) => ({ ...f, [entry.key]: e.target.value }))}
                  />
                  <button className="btn btn-primary btn-sm" onClick={() => handleSave(entry.key, entry.value)}>Save</button>
                  <button className="btn btn-secondary btn-sm" onClick={() => setPendingReset(entry.key)}>Reset</button>
                </div>
              </div>
            )
          })}
        </div>
      )}
      {envData && (
        <div className="readonly-section" style={{ marginTop: 16 }}>
          <div className="section-label">Environment</div>
          <div style={{ display: 'grid', gridTemplateColumns: '220px 1fr', gap: '4px 12px', marginTop: 8, fontSize: 12 }}>
            {Object.entries(envData).map(([k, v]) => (
              <Fragment key={k}>
                <span className="dim">{k}</span>
                <span className="mono">{v}</span>
              </Fragment>
            ))}
          </div>
        </div>
      )}

      <ConfirmDialog
        open={pendingReset !== null}
        onClose={() => setPendingReset(null)}
        onConfirm={doReset}
        title="Reset override?"
        message={
          <>
            Reset override for <span className="mono">{pendingReset}</span>? The runtime value will revert
            to the env-file or default. Active connections are not affected.
          </>
        }
        confirmLabel="Reset"
        variant="primary"
      />
    </div>
  )
}