diff --git a/.env.example b/.env.example index b3a2c95..b85c6ca 100644 --- a/.env.example +++ b/.env.example @@ -1,17 +1,23 @@ -# Super Proxy — example environment (safe defaults, no real secrets) +# Super Proxy example environment +# Copy this file to .env. Never commit .env or real credentials. NODE_ENV=development PORT=8080 DATABASE_PATH=./data/super-proxy.sqlite ADMIN_EMAIL=admin@localhost -# Optional branding used by some upstreams (e.g. OpenRouter headers) +# Required for Docker Compose and strongly recommended for every deployment. +# Generate each value independently with: openssl rand -hex 32 +SESSION_SECRET= +DEV_ADMIN_KEY= + +# Optional branding used by some upstreams (for example, OpenRouter headers). GATEWAY_NAME=Super Proxy GATEWAY_PUBLIC_URL=http://localhost:8080 OPENROUTER_HTTP_REFERER=http://localhost:8080 OPENROUTER_APP_TITLE=Super Proxy -# Upstream base URL overrides (optional) +# Optional upstream base URL overrides. ANTHROPIC_UPSTREAM_URL=https://api.anthropic.com OPENAI_PLATFORM_UPSTREAM_URL=https://api.openai.com/v1 OPENAI_UPSTREAM_URL=https://chatgpt.com/backend-api @@ -25,10 +31,10 @@ DEEPGRAM_UPSTREAM_URL=https://api.deepgram.com/v1 FISH_UPSTREAM_URL=https://api.fish.audio XAI_UPSTREAM_URL=https://api.x.ai/v1 -# Provider credentials: configure via dashboard/admin or your SecretStore. -# Do not put production keys in git. +# Configure provider credentials through the dashboard/admin API or a SecretStore. +# Do not put production keys in tracked files. -# Optional features +# Optional features. HEADROOM_ENABLED=false NORMALIZE_GLM=false NORMALIZE_KIMI=false diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 34de355..ea399b4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,6 +3,7 @@ on: push: branches: [main] pull_request: + jobs: verify: runs-on: ubuntu-latest @@ -16,3 +17,27 @@ jobs: - run: npm run build - run: npm test - run: bash scripts/secret-scan.sh + + compose-smoke: + runs-on: ubuntu-latest + env: + SESSION_SECRET: ci-session-secret-not-for-production + DEV_ADMIN_KEY: ci-dev-admin-key-not-for-production + steps: + - uses: actions/checkout@v4 + - name: Validate Compose configuration + run: docker compose config --quiet + - name: Build and start Compose service + run: docker compose up --build --wait --wait-timeout 180 + - name: Verify health, runtime user, persistence, and notices + run: | + curl --fail --silent --show-error http://127.0.0.1:8080/health + test "$(docker compose exec -T super-proxy id -u)" = "10001" + docker compose exec -T super-proxy sh -c 'test -w /app/data && test -f /app/data/super-proxy.sqlite' + docker compose exec -T super-proxy sh -c 'test -f /app/LICENSE && test -f /app/NOTICE' + - name: Show Compose logs on failure + if: failure() + run: docker compose logs --no-color + - name: Stop Compose service + if: always() + run: docker compose down --volumes diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 2b3e69a..a4b245f 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -1,30 +1,61 @@ # Contributor Covenant Code of Conduct -## Our Pledge +## Our pledge -We pledge to make participation in Super Proxy a harassment-free experience for everyone. +We pledge to make participation in Super Proxy a harassment-free experience +for everyone, regardless of age, body size, visible or invisible disability, +ethnicity, sex characteristics, gender identity and expression, level of +experience, education, socioeconomic status, nationality, personal +appearance, race, caste, color, religion, or sexual identity and orientation. -## Our Standards +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. -Examples of behavior that contributes to a positive environment: +## Our standards -- Demonstrating empathy and kindness -- Being respectful of differing opinions -- Giving and accepting constructive feedback -- Focusing on what is best for the community +Examples of behavior that contributes to a positive environment include: -Unacceptable behavior includes: +- demonstrating empathy and kindness; +- respecting differing opinions, viewpoints, and experiences; +- giving and gracefully accepting constructive feedback; +- accepting responsibility, apologizing, and learning from mistakes; and +- focusing on what is best for the community. -- Harassment, trolling, or insulting comments -- Publishing others' private information -- Other conduct which could reasonably be considered inappropriate +Examples of unacceptable behavior include: + +- sexualized language or imagery, or unwelcome sexual attention; +- trolling, insulting or derogatory comments, and personal or political attacks; +- public or private harassment; +- publishing another person's private information without permission; and +- other conduct that could reasonably be considered inappropriate in a + professional setting. + +## Enforcement responsibilities + +Project maintainers are responsible for clarifying and enforcing these +standards. They may remove, edit, or reject comments, commits, code, issues, +and other contributions that do not align with this Code of Conduct. + +## Scope + +This Code of Conduct applies in project spaces and when an individual is +publicly representing the project or its community. ## Enforcement -Report incidents to the maintainers via the private security/contact channel listed in `SECURITY.md`. +Report conduct incidents to the maintainers. When a report must remain +private, use the repository's +[private reporting form](https://github.com/Nextbasedev/super-proxy/security/advisories/new) +and begin the report title with `Code of Conduct`. Do not include secrets or +unrelated production data. -Maintainers will review and respond as appropriate. +Maintainers will protect the privacy and safety of reporters as far as +practical, investigate promptly, and apply a proportionate response. A +maintainer who is the subject of a report must recuse themselves from handling +it. ## Attribution -This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org), version 2.1. +This Code of Conduct is adapted from the +[Contributor Covenant](https://www.contributor-covenant.org/version/2/1/code_of_conduct.html), +version 2.1. diff --git a/Dockerfile b/Dockerfile index 9ae3ad4..f8c04f4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,7 +14,7 @@ RUN npm run build FROM node:22-bookworm-slim AS runtime WORKDIR /app ENV NODE_ENV=production \ - PORT=4580 \ + PORT=8080 \ DATABASE_PATH=/app/data/super-proxy.sqlite COPY package.json package-lock.json ./ @@ -23,23 +23,17 @@ RUN npm ci --omit=dev --no-audit --no-fund && npm cache clean --force COPY --from=build /app/dist ./dist COPY public ./public COPY docs ./docs -COPY README.md ./.env.example ./ +COPY README.md LICENSE NOTICE .env.example ./ -# Create a dedicated non-root user+group with a FIXED uid/gid (10001:10001) to -# match the thread-agent convention on this box. release-process requires the -# container to run as non-root. +# Keep the runtime unprivileged and give its fixed uid/gid ownership of the +# persistent SQLite directory. A fresh named volume inherits this ownership. RUN groupadd --gid 10001 app \ - && useradd --uid 10001 --gid 10001 --no-create-home --shell /usr/sbin/nologin app - -# Ensure the data dir (sqlite lives here) and app tree are owned by the runtime -# uid:gid so migrations/server can write the DB. -# NOTE (prod): the host bind mount at /app/data must ALSO be chown'd to -# 10001:10001 on the host, otherwise the container cannot write its sqlite DB. -# See docs/HARDENING.md. -RUN mkdir -p /app/data && chown -R 10001:10001 /app/data /app -VOLUME ["/app/data"] -EXPOSE 4580 + && useradd --uid 10001 --gid 10001 --no-create-home --shell /usr/sbin/nologin app \ + && mkdir -p /app/data \ + && chown 10001:10001 /app/data +VOLUME ["/app/data"] +EXPOSE 8080 USER 10001:10001 CMD ["sh", "-c", "node dist/db/migrate.js && node dist/server.js"] diff --git a/NOTICE b/NOTICE index f3b0c9a..03d7e31 100644 --- a/NOTICE +++ b/NOTICE @@ -2,4 +2,5 @@ Super Proxy Copyright 2026 Super Proxy contributors This product includes software developed by Super Proxy contributors. -Licensed under the Apache License, Version 2.0. See LICENSE. +Licensed under the Apache License, Version 2.0. See the LICENSE file included +with the source distribution and container image. diff --git a/OSS-SCOPE.md b/OSS-SCOPE.md index e797e16..9408764 100644 --- a/OSS-SCOPE.md +++ b/OSS-SCOPE.md @@ -1,84 +1,52 @@ -# Super Proxy — OSS Scope +# Super Proxy project scope -Working name: **super-proxy** -Source baseline: super-proxy `origin/main` @ `787d2dd` -Status: **local only** — do not create or push a public GitHub repository until Don explicitly approves. +Super Proxy is an open-source, self-hosted AI gateway. It provides one +operator-managed service for routing authenticated client requests to multiple +model providers. -## Wave 1 (in scope) +## Included -- Multi-provider AI gateway core -- OpenAI-compatible and Anthropic-compatible HTTP surfaces -- Provider adapters / pools / governor +- OpenAI-compatible and Anthropic-compatible HTTP APIs - Streaming and non-streaming request paths -- API token authentication +- Provider adapters, account pools, and concurrency controls +- Gateway API tokens with policy and usage identity - SQLite persistence and migrations -- Usage / cost accounting and basic policy limits -- Model catalog endpoint(s) -- Health and metrics endpoints -- Docker Compose self-host path -- Operator dashboard (`public/`) -- Fusion only if it has no private control-plane dependency +- Usage, cost, health, metrics, and basic policy surfaces +- Model discovery endpoints +- Built-in operator dashboard +- Docker and Docker Compose deployment +- Small extension contracts for authentication, providers, secrets, and plugins -## Wave 1 (out of scope) +## Not included -- Aside control plane (`aside*`) -- OC fleet integration (`oc-fleet*`) -- Production release-process / host-specific runbooks -- Company emails, Firebase project IDs, internal domains, account labels -- Private control-plane UIs only — **operator dashboard in `public/` is in scope** -- Private git history from production repository +The public project does not include organization-specific infrastructure, +host inventories, deployment credentials, proprietary control planes, or +private operational runbooks. Integrations that require those systems belong +in separate deployments or plugins and must not be prerequisites for the +self-hosted gateway. -## Architecture contracts (wave 1) +Super Proxy is not a hosted service and does not provide provider accounts or +model-provider credentials. Operators remain responsible for upstream terms, +network access, data handling, backups, and deployment security. -Prefer a single-package TypeScript gateway: +## Architecture principles -```text -src/ - app.ts # buildApp() - server.ts # listen only - config/ - core/ - auth/ - secrets/ - providers/ - routes/ # migrated from proxy/* over time - usage/ - db/ - monitoring/ - plugins/ -``` - -Minimum extension points (keep small and real): +The project favors a single TypeScript service with explicit Fastify routes, +provider modules, and SQLite persistence. Public extension points are kept +small and implementation-driven: - `GatewayPlugin` - `ProviderAdapter` - `AuthProvider` - `SecretStore` -Do **not** invent unused abstraction layers. - -## Agent ownership - -| Agent | Owns | Must not touch | -|---|---|---| -| A runtime | `src/providers/**`, `src/proxy/**`→routes, `src/normalize/**`, `src/fusion/**`, related tests | docs package metadata beyond need; auth/db ownership files | -| B platform | `src/auth/**`, `src/db/**`, `src/admin/**`, usage/policy/cost, monitoring sanitize, config defaults | provider transport implementations; marketing docs body | -| C oss-dx | README, ARCHITECTURE, CONTRIBUTING, LICENSE, SECURITY, .env.example, Docker polish, CI local, secret-scan, examples | runtime business logic | - -## Non-negotiable constraints - -- Local filesystem only under `projects/super-proxy*` -- No `gh repo create`, no public visibility change, no push to GitHub -- No secrets in tree; no real tokens in tests -- No private markers: aside, oc-fleet, infinitycorp, Daxitdon, ampere project ids, release-process hosts -- Preserve behavior of public gateway routes where practical -- All commits signed if agent environment supports signing; otherwise normal commits and parent will re-sign on integrate -- Fresh git history only (already initialized in super-proxy) +New abstractions should solve a demonstrated integration need. Public gateway +routes should remain backward-compatible where practical; pre-1.0 interfaces +may still evolve with release notes and migration guidance. -## Definition of done (integration) +## Contribution boundary -- `npm ci && npm run build && npm test` pass -- Docker health smoke passes -- Secret/internal-reference scan clean -- Docs enable clean-machine quickstart -- Parent reports to Don; still not public +Contributions must not include live credentials, private customer data, +internal hostnames, employee-only identifiers, or copied proprietary source +history. See [`CONTRIBUTING.md`](./CONTRIBUTING.md), +[`SECURITY.md`](./SECURITY.md), and [`LICENSE`](./LICENSE). diff --git a/README.md b/README.md index 4504502..68dde2b 100644 --- a/README.md +++ b/README.md @@ -1,80 +1,98 @@ # Super Proxy -**Open-source multi-provider AI gateway.** -One self-hosted endpoint for OpenAI-compatible and Anthropic-compatible APIs — with auth, usage limits, streaming, provider pools, and a built-in dashboard. +**Open-source multi-provider AI gateway.** -> Alpha self-host release (local tree). Not published to a public GitHub remote until explicitly approved. +Super Proxy provides one self-hosted endpoint for OpenAI-compatible and +Anthropic-compatible APIs, with authentication, usage limits, streaming, +provider pools, and a built-in operator dashboard. [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](./LICENSE) [![Node](https://img.shields.io/badge/node-%3E%3D20-brightgreen.svg)](./package.json) ---- - ## Why Super Proxy? | Pain | Super Proxy | -|---|---| -| Every provider has a different API shape | Unified **OpenAI** + **Anthropic** surfaces | -| Keys scattered across tools | **API tokens** with per-token limits | -| No visibility | **Usage**, cost accounting, health, dashboard | -| One account rate-limit kills the app | **Provider pools** + governor | -| Want Claude Code / OpenAI SDKs unchanged | Drop-in base URL override | - ---- +| --- | --- | +| Every provider has a different API shape | Unified OpenAI and Anthropic surfaces | +| Keys are scattered across tools | Gateway API tokens with per-token limits | +| Operators lack visibility | Usage, cost accounting, health, and dashboard views | +| One account rate limit stops an app | Provider pools and concurrency controls | +| Existing SDKs need a stable endpoint | Drop-in base URL overrides | ## Features -- **OpenAI-compatible** chat/completions & related routes -- **Anthropic-compatible** `/v1/messages` (+ raw Anthropic passthrough where enabled) -- **Multi-provider**: Anthropic, OpenAI/Codex, Groq, Cerebras, Kimi, GLM, Gemini, OpenRouter, xAI, Deepgram, Fish, Runpod, search, Fusion -- **Streaming & non-streaming** -- **Token auth** + admin APIs -- **SQLite** persistence (simple self-host default) -- **Usage / policy / cost** hooks -- **Web dashboard** (`public/`) for operators -- **Docker Compose** one-command start - ---- +- OpenAI-compatible chat, completions, responses, and related routes +- Anthropic-compatible `/v1/messages` routes +- Multiple upstream providers, including Anthropic, OpenAI, Groq, Cerebras, + Kimi, GLM, Gemini, OpenRouter, xAI, Deepgram, Fish Audio, and Runpod +- Streaming and non-streaming responses +- Gateway token authentication and admin APIs +- SQLite persistence +- Usage, policy, and cost controls +- Built-in operator dashboard +- Docker Compose deployment ## Quick start ### Prerequisites -- Node.js **20+** -- npm 10+ +Choose either: + +- Node.js 20+ and npm 10+; or +- Docker with Docker Compose v2. -### Local +`openssl` is used below to generate independent random secrets. + +### Configure secrets ```bash -git clone super-proxy +git clone https://github.com/Nextbasedev/super-proxy.git cd super-proxy cp .env.example .env +sed -i.bak "s/^SESSION_SECRET=.*/SESSION_SECRET=$(openssl rand -hex 32)/" .env +sed -i.bak "s/^DEV_ADMIN_KEY=.*/DEV_ADMIN_KEY=$(openssl rand -hex 32)/" .env +rm -f .env.bak +``` + +Do not reuse the two values or commit `.env`. `SESSION_SECRET` signs browser +sessions. `DEV_ADMIN_KEY` is a bootstrap administrator credential and should be +protected like a password. + +### Start with Node.js + +```bash npm ci npm run build npm start ``` -Health: +### Start with Docker Compose ```bash -curl -sS http://127.0.0.1:8080/health +docker compose up --build -d +docker compose ps +docker compose exec super-proxy id +curl -fsS http://127.0.0.1:8080/health ``` -Dashboard: [http://127.0.0.1:8080/](http://127.0.0.1:8080/) +The container runs as uid/gid `10001:10001`. Compose stores SQLite at +`/app/data/super-proxy.sqlite` in the writable `super-proxy-data` named volume. -### Docker Compose +### Bootstrap the dashboard -```bash -cp .env.example .env -docker compose up --build -d -curl -sS http://127.0.0.1:8080/health -``` +1. Open [http://127.0.0.1:8080/](http://127.0.0.1:8080/). +2. Expand **Use dev admin key instead**. +3. Paste the `DEV_ADMIN_KEY` value from `.env` and press **Enter**. +4. Add a user and issue a gateway API token from the **Identity** view. +5. Store the displayed `sp_...` token immediately; it is not shown again. ---- +The dev admin key sends the `x-admin-key` header and is intended for initial +self-host setup. Configure your normal authentication path and restrict the +dashboard to a trusted network before exposing the service beyond localhost. ## Example requests -Set: +Set the URL and the gateway token created in the dashboard: ```bash export SUPER_PROXY_URL=http://127.0.0.1:8080 @@ -108,70 +126,50 @@ curl -sS "$SUPER_PROXY_URL/v1/messages" \ }' ``` -See `examples/` for copy-paste scripts. - ---- +See [`examples/`](./examples) for scripts. ## Configuration | Variable | Default | Purpose | -|---|---|---| +| --- | --- | --- | | `PORT` | `8080` | HTTP port | | `DATABASE_PATH` | `./data/super-proxy.sqlite` | SQLite file | | `ADMIN_EMAIL` | `admin@localhost` | Bootstrap admin identity | +| `SESSION_SECRET` | none | Browser session signing secret | +| `DEV_ADMIN_KEY` | none | Self-host admin bootstrap key | | `NODE_ENV` | `development` | Runtime mode | -| `*_UPSTREAM_URL` | provider defaults | Override upstream bases | - -Full list: [`.env.example`](./.env.example) +| `*_UPSTREAM_URL` | provider defaults | Optional upstream URL overrides | -**Never commit real API keys.** Provider credentials belong in your environment or admin-configured secret store. +See [`.env.example`](./.env.example) and +[`docs/configuration.md`](./docs/configuration.md) for details. Never commit +provider credentials or real gateway tokens. ---- - -## Architecture +## Architecture and documentation ```text -Client SDK / Claude Code / curl - │ - ▼ -┌───────────────────────┐ -│ Super Proxy │ -│ auth · policy · route│ -│ pools · usage · admin│ -└───────────┬───────────┘ - │ - ▼ +Client SDK / CLI / curl + | + v ++-----------------------+ +| Super Proxy | +| auth, policy, routing | +| pools, usage, admin | ++-----------+-----------+ + | + v Upstream model providers ``` -Extension points (stable, minimal): - -- `GatewayPlugin` -- `ProviderAdapter` -- `AuthProvider` -- `SecretStore` - -Details: [`ARCHITECTURE.md`](./ARCHITECTURE.md) · scope: [`OSS-SCOPE.md`](./OSS-SCOPE.md) - -More docs: +Public extension points include `GatewayPlugin`, `ProviderAdapter`, +`AuthProvider`, and `SecretStore`. +- [Architecture](./ARCHITECTURE.md) +- [Project scope](./OSS-SCOPE.md) - [Getting started](./docs/getting-started.md) - [Configuration](./docs/configuration.md) - [Deployment](./docs/deployment.md) - [Providers](./docs/providers.md) -- [Auth](./docs/auth.md) - ---- - -## Dashboard - -The built-in operator console is served from `public/`: - -- session/bootstrap against the local gateway -- token and usage oriented workflows -- no external control-plane dependency required for basic self-host - ---- +- [Authentication](./docs/auth.md) ## Development @@ -179,21 +177,21 @@ The built-in operator console is served from `public/`: npm ci npm run build npm test +shellcheck scripts/*.sh examples/*.sh ./scripts/secret-scan.sh ``` -Contributing: [`CONTRIBUTING.md`](./CONTRIBUTING.md) -Security: [`SECURITY.md`](./SECURITY.md) +See [`CONTRIBUTING.md`](./CONTRIBUTING.md), +[`CODE_OF_CONDUCT.md`](./CODE_OF_CONDUCT.md), and +[`SECURITY.md`](./SECURITY.md). ---- +## Release status -## License +Super Proxy is pre-1.0. Interfaces and configuration may change between minor +releases; use Git tags and GitHub Releases as the source of truth for published +versions. -Apache License 2.0 — see [`LICENSE`](./LICENSE). - ---- - -## Status +## License -Wave 1 focuses on a **complete self-host gateway + dashboard**. -Private production control-plane integrations (fleet orchestration, company-specific deploy wiring) stay out of this tree on purpose. +Licensed under the Apache License 2.0. See [`LICENSE`](./LICENSE) and +[`NOTICE`](./NOTICE). diff --git a/SECURITY.md b/SECURITY.md index 19a1659..ed05cf4 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -2,40 +2,50 @@ ## Supported versions -| Version | Supported | -|---|---| -| `0.x` (main) | Best-effort security fixes | +Super Proxy is pre-1.0. Security fixes are applied to the current `main` branch +and included in the next tagged release. Older commits and unmaintained forks +are not supported. + +| Release line | Supported | +| --- | --- | +| Current `main` / latest tagged release | Yes | +| Older snapshots | No | ## Reporting a vulnerability -Please report security issues **privately**. +Please report vulnerabilities privately through a +[GitHub Security Advisory](https://github.com/Nextbasedev/super-proxy/security/advisories/new). +Do not open a public issue for a suspected vulnerability. -Include: +Include, when possible: -- Super Proxy version / commit -- Reproduction steps -- Impact assessment -- Any logs **with secrets redacted** +- the affected Super Proxy release or commit; +- reproduction steps or a minimal proof of concept; +- the expected impact and attack prerequisites; and +- relevant logs with secrets and personal data removed. -Do **not** attach live API keys, session cookies, or production database dumps. +Do not attach live API keys, session cookies, provider credentials, or +production database dumps. Maintainers will acknowledge a report as soon as +practical, coordinate remediation with the reporter, and disclose a fix after +users have had a reasonable opportunity to update. -## Hardening checklist (operators) +## Operator hardening checklist -- [ ] Run behind TLS-terminating reverse proxy -- [ ] Use strong admin credentials / SSO when available -- [ ] Issue least-privilege API tokens -- [ ] Set provider keys via environment or a real secret manager -- [ ] Restrict dashboard exposure (VPN / tailnet / IP allowlist) -- [ ] Back up and encrypt SQLite volumes -- [ ] Rotate tokens after staff changes -- [ ] Keep Node and dependencies updated +- [ ] Run behind a TLS-terminating reverse proxy. +- [ ] Generate independent, high-entropy `SESSION_SECRET` and `DEV_ADMIN_KEY` values. +- [ ] Restrict dashboard exposure to a trusted network or authenticated access layer. +- [ ] Issue least-privilege API tokens and revoke unused tokens. +- [ ] Store provider credentials outside tracked files. +- [ ] Back up and encrypt the SQLite volume. +- [ ] Keep the container base image, Node.js, and dependencies updated. ## Secret scanning -Before release builds: +Before release builds, run: ```bash ./scripts/secret-scan.sh ``` -The scan fails on common token shapes and internal markers that must not ship. +The scan checks common credential shapes and repository-specific private +markers. It supplements, but does not replace, dependency and code review. diff --git a/docker-compose.yml b/docker-compose.yml index c4828bd..e7a2605 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,12 +6,14 @@ services: environment: NODE_ENV: production PORT: 8080 - DATABASE_PATH: /data/super-proxy.sqlite + DATABASE_PATH: /app/data/super-proxy.sqlite ADMIN_EMAIL: ${ADMIN_EMAIL:-admin@localhost} + SESSION_SECRET: "${SESSION_SECRET:?Set SESSION_SECRET in .env; see README.md}" + DEV_ADMIN_KEY: "${DEV_ADMIN_KEY:?Set DEV_ADMIN_KEY in .env; see README.md}" GATEWAY_NAME: ${GATEWAY_NAME:-Super Proxy} GATEWAY_PUBLIC_URL: ${GATEWAY_PUBLIC_URL:-http://localhost:8080} volumes: - - super-proxy-data:/data + - super-proxy-data:/app/data healthcheck: test: ["CMD", "node", "-e", "fetch('http://127.0.0.1:8080/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"] interval: 10s diff --git a/docs/auth.md b/docs/auth.md index 2cf59b3..3dd510c 100644 --- a/docs/auth.md +++ b/docs/auth.md @@ -1,35 +1,64 @@ # Authentication -## API tokens +Super Proxy has separate credentials for application traffic, browser +sessions, and administrator bootstrap. Do not reuse values across these roles. -Machine clients should use gateway API tokens: +## Gateway API tokens + +Machine clients should use a gateway API token: ```http Authorization: Bearer sp_... ``` -or (Anthropic-style clients): +Anthropic-style clients may send the same token as: ```http x-api-key: sp_... ``` -Tokens are validated in `src/auth/*` and carry policy/usage identity. +Tokens carry user, policy, and usage identity. Create separate tokens for +separate clients so they can be limited and revoked independently. + +## Dashboard sessions + +When Firebase authentication is configured, the dashboard verifies the +identity token and creates an HTTP-only session cookie. `SESSION_SECRET` signs +that cookie and must be a unique, high-entropy value in every deployment. + +Generate one with: + +```bash +openssl rand -hex 32 +``` -## Dashboard auth +Changing `SESSION_SECRET` invalidates existing dashboard sessions. -Browser operators authenticate through dashboard auth routes (`src/auth/dashboard-auth.ts`). -Cookie/session details are implementation concerns — operators should: +## Dev admin bootstrap key -- serve dashboard only on trusted networks or behind SSO/TLS -- rotate admin access if a browser session is compromised +Self-host operators can bootstrap without Firebase by setting a separate +`DEV_ADMIN_KEY`. It authorizes admin routes through the `x-admin-key` header. -## Admin routes +1. Generate a value with `openssl rand -hex 32` and place it in `.env`. +2. Open the dashboard and expand **Use dev admin key instead**. +3. Paste the key and press **Enter**. +4. Create a user and gateway token from the **Identity** view. + +The dev key does not create a normal dashboard session. Treat it as a root +administrator password, keep the dashboard on a trusted network, and rotate or +remove the key when another administrator authentication path is established. + +For scripted maintenance, the equivalent header is: + +```http +x-admin-key: your-generated-dev-admin-key +``` -Admin HTTP APIs require admin authentication (header/key/session depending on config). -Never expose admin ports directly to the public internet without an access layer. +Never send this header to provider APIs or distribute it to application +clients. -## Secret material +## Provider credentials -Use `SecretStore` (`src/plugins/types.ts`, `src/secrets/`) for non-token secrets. -Default implementation reads process environment. +Provider credentials are upstream secrets, not gateway API tokens. Configure +them through the dashboard/admin API or a `SecretStore` integration. Do not +commit them to `.env.example`, source files, examples, or issue reports. diff --git a/docs/configuration.md b/docs/configuration.md index d7f94d7..473876a 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -6,42 +6,70 @@ Super Proxy is configured primarily through environment variables. cp .env.example .env ``` -## Core +## Core settings | Variable | Default | Description | -|---|---|---| +| --- | --- | --- | | `PORT` | `8080` | HTTP listen port | -| `HOST` binding | `0.0.0.0` in server | Set edge TLS separately | -| `DATABASE_PATH` | `./data/super-proxy.sqlite` | SQLite path | -| `ADMIN_EMAIL` | `admin@localhost` | Bootstrap admin email | -| `NODE_ENV` | `development` | `production` recommended in deploy | -| `GATEWAY_NAME` | `Super Proxy` | Display/branding name | -| `GATEWAY_PUBLIC_URL` | `http://localhost:8080` | Public base URL for callbacks/headers | +| `DATABASE_PATH` | `./data/super-proxy.sqlite` | SQLite database path | +| `ADMIN_EMAIL` | `admin@localhost` | Admin identity allowed to bootstrap through configured browser auth | +| `SESSION_SECRET` | none | Secret used to sign dashboard session cookies | +| `DEV_ADMIN_KEY` | none | Header-based bootstrap administrator key | +| `NODE_ENV` | `development` | Use `production` for deployed instances | +| `GATEWAY_NAME` | `Super Proxy` | Display and integration name | +| `GATEWAY_PUBLIC_URL` | `http://localhost:8080` | Public base URL used by integrations | + +The server listens on all interfaces. Apply network policy and TLS at your +reverse proxy or container platform. + +## Generate bootstrap secrets + +Generate the values independently: + +```bash +openssl rand -hex 32 +openssl rand -hex 32 +``` + +Paste one output after `SESSION_SECRET=` and the other after `DEV_ADMIN_KEY=` +in `.env`. Docker Compose rejects an empty value for either variable. Rotate +both before deploying a copied or shared environment. + +`DEV_ADMIN_KEY` grants administrator access through the `x-admin-key` header. +It is intended for self-host bootstrap and trusted maintenance, not as an API +token for applications. ## Upstream overrides -Each provider accepts an optional `*_UPSTREAM_URL` (see `.env.example`). +Each provider accepts an optional `*_UPSTREAM_URL`; see [`.env.example`](../.env.example). +Leave these unset or at their documented defaults unless a provider requires a +custom endpoint. ## Feature flags | Variable | Default | Description | -|---|---|---| -| `HEADROOM_ENABLED` | `false` | Context compression sidecar | -| `NORMALIZE_GLM` | `false` | GLM stream normalization | -| `NORMALIZE_KIMI` | `false` | Kimi stream normalization | -| `MONITOR_RETENTION_ENABLED` | `false` | Monitoring retention jobs | +| --- | --- | --- | +| `HEADROOM_ENABLED` | `false` | Enable the configured context compression sidecar | +| `NORMALIZE_GLM` | `false` | Enable GLM stream normalization | +| `NORMALIZE_KIMI` | `false` | Enable Kimi stream normalization | +| `MONITOR_RETENTION_ENABLED` | `false` | Enable monitoring retention jobs | -## Secrets +## Credentials and tokens -Do not put production provider keys in git. +Do not put production provider keys, session cookies, or gateway tokens in +tracked files. -- Prefer environment variables or a `SecretStore` plugin (`src/secrets`) -- Issue per-client gateway API tokens from the dashboard/admin API -- Rotate tokens after staff or integration changes +- Configure provider accounts through the dashboard/admin API or a + `SecretStore` integration. +- Issue a separate gateway API token for each client or user. +- Rotate bootstrap and API credentials after disclosure or staff changes. ## Database -SQLite is the default for simple self-host. +SQLite is the default self-host database. -- Back up `DATABASE_PATH` regularly -- Put the file on persistent storage in Docker (`super-proxy-data` volume) +- Back up `DATABASE_PATH` regularly and test restores. +- Docker Compose uses `/app/data/super-proxy.sqlite` in the + `super-proxy-data` named volume. +- The container runs as uid/gid `10001:10001`; custom bind mounts must be + writable by that identity. diff --git a/docs/deployment.md b/docs/deployment.md index 9c44fa8..f83b8e8 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -1,51 +1,85 @@ # Deployment -## Docker Compose (recommended) +## Docker Compose + +Configure independent secrets before starting: ```bash cp .env.example .env +sed -i.bak "s/^SESSION_SECRET=.*/SESSION_SECRET=$(openssl rand -hex 32)/" .env +sed -i.bak "s/^DEV_ADMIN_KEY=.*/DEV_ADMIN_KEY=$(openssl rand -hex 32)/" .env +rm -f .env.bak docker compose up --build -d +docker compose ps curl -fsS http://127.0.0.1:8080/health ``` -Open the dashboard at `http://127.0.0.1:8080/`. +Open the dashboard at `http://127.0.0.1:8080/`, expand **Use dev admin key +instead**, paste `DEV_ADMIN_KEY`, and press **Enter**. -### Persistence +### Persistence and runtime identity -Compose mounts volume `super-proxy-data` to `/data` and sets: +Compose mounts the `super-proxy-data` named volume at `/app/data` and sets: ```text -DATABASE_PATH=/data/super-proxy.sqlite +DATABASE_PATH=/app/data/super-proxy.sqlite +``` + +The image runs as uid/gid `10001:10001`. A new named volume inherits the +writable `/app/data` ownership from the image. If you replace the named volume +with a host bind mount, create the directory and grant uid/gid `10001:10001` +write access before starting the container. + +Confirm the runtime contract: + +```bash +docker compose exec super-proxy id +docker compose exec super-proxy sh -c \ + 'test -w /app/data && test -f /app/data/super-proxy.sqlite' ``` ### Healthcheck -The container healthcheck hits `/health`. +The container healthcheck requests `/health`. The endpoint reports unhealthy +until database migrations are current. + +```bash +docker compose ps +docker compose logs super-proxy +``` -## Bare metal / systemd +## Bare metal or systemd ```bash npm ci npm run build -NODE_ENV=production PORT=8080 DATABASE_PATH=/var/lib/super-proxy/db.sqlite npm start +NODE_ENV=production \ +PORT=8080 \ +DATABASE_PATH=/var/lib/super-proxy/super-proxy.sqlite \ +npm start ``` -Run under your process manager. Terminate TLS at Caddy/Nginx/Traefik. +Ensure the process environment also supplies `SESSION_SECRET` and the chosen +administrator authentication configuration. Run under an unprivileged service +account and terminate TLS at a reverse proxy such as Caddy, Nginx, or Traefik. ## Hardening checklist -1. Do not expose admin/dashboard to the open internet without auth edge controls -2. Use strong API tokens; revoke unused tokens -3. Keep provider credentials in env/secret manager -4. Restrict outbound egress if required by policy -5. Back up SQLite and test restore -6. Pin image digests in production if you publish one +1. Restrict the dashboard to a trusted network or authenticated access layer. +2. Use TLS for every non-local deployment. +3. Protect and rotate `SESSION_SECRET`, `DEV_ADMIN_KEY`, gateway tokens, and + provider credentials. +4. Restrict outbound egress when required by policy. +5. Back up SQLite and test restore procedures. +6. Pin image digests in production deployments. +7. Monitor `/health` and container restarts. ## Upgrades -1. Pull new version -2. `npm ci && npm run build` (or rebuild image) -3. Restart process/container -4. Confirm `/health` and migration id +1. Read release notes and back up the database. +2. Pull the intended tagged version. +3. Rebuild the image or run `npm ci && npm run build`. +4. Restart the service; migrations run before the server starts. +5. Confirm `/health`, dashboard access, and a scoped provider request. -Migrations run on startup via `src/db/migrate.ts`. +Use Git tags and GitHub Releases as the source of truth for published versions. diff --git a/docs/getting-started.md b/docs/getting-started.md index 3fb8e19..3205e44 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -1,43 +1,76 @@ # Getting started -## 1. Install +## 1. Clone and configure ```bash +git clone https://github.com/Nextbasedev/super-proxy.git +cd super-proxy cp .env.example .env +sed -i.bak "s/^SESSION_SECRET=.*/SESSION_SECRET=$(openssl rand -hex 32)/" .env +sed -i.bak "s/^DEV_ADMIN_KEY=.*/DEV_ADMIN_KEY=$(openssl rand -hex 32)/" .env +rm -f .env.bak +``` + +The values must be independent. Keep `.env` private: `SESSION_SECRET` signs +browser sessions, while `DEV_ADMIN_KEY` grants bootstrap administrator access. + +## 2. Start the gateway + +With Node.js 20+: + +```bash npm ci npm run build +npm start ``` -## 2. Start +Or with Docker Compose: ```bash -npm start -# -> http://127.0.0.1:8080 +docker compose up --build -d +``` + +Verify readiness: + +```bash +curl -fsS http://127.0.0.1:8080/health ``` -## 3. Open dashboard +## 3. Bootstrap the dashboard -Visit `http://127.0.0.1:8080/` and complete local admin/bootstrap flow for your environment. +1. Visit `http://127.0.0.1:8080/`. +2. Expand **Use dev admin key instead**. +3. Paste the `DEV_ADMIN_KEY` value from `.env` and press **Enter**. +4. Open **Identity**, create a user, and issue that user an API token. +5. Copy the displayed token immediately. The raw token is shown only once. -## 4. Create an API token +The dev key uses the `x-admin-key` request header. It does not create a browser +session and should not be shared with regular users. -Use the dashboard or admin API to create a token. Export it: +## 4. Send a test request ```bash -export SUPER_PROXY_API_KEY=sp_... export SUPER_PROXY_URL=http://127.0.0.1:8080 +export SUPER_PROXY_API_KEY=sp_your_token_here +bash examples/basic-chat.sh ``` -## 5. Send a test chat +Provider requests require a corresponding upstream account or credential. +Configure providers from the dashboard after bootstrap. + +## Docker data + +Compose persists the database in the `super-proxy-data` named volume, mounted +at `/app/data` for the unprivileged uid/gid `10001:10001` runtime user. + +Stop the service without deleting data: ```bash -bash examples/basic-chat.sh +docker compose down ``` -## Docker +Delete the service and its data only when intentional: ```bash -docker compose up --build -d +docker compose down --volumes ``` - -Data persists in the `super-proxy-data` volume (see `docker-compose.yml`). diff --git a/docs/providers.md b/docs/providers.md index 42c8d4d..b041c42 100644 --- a/docs/providers.md +++ b/docs/providers.md @@ -4,31 +4,41 @@ Super Proxy fronts multiple upstream providers behind stable client APIs. ## Client surfaces -| Surface | Typical paths | Auth header | -|---|---|---| -| OpenAI-compatible | `/v1/chat/completions`, `/v1/responses`, … | `Authorization: Bearer ` | -| Anthropic-compatible | `/v1/messages` | `x-api-key: ` | -| Provider-specific | `/v1/groq/...`, `/v1/xai/...`, etc. | gateway token | +| Surface | Typical paths | Gateway auth header | +| --- | --- | --- | +| OpenAI-compatible | `/v1/chat/completions`, `/v1/responses`, and related routes | `Authorization: Bearer sp_...` | +| Anthropic-compatible | `/v1/messages` | `x-api-key: sp_...` | +| Provider-specific | `/v1/groq/...`, `/v1/xai/...`, and related routes | Gateway token as documented by the route | -Exact routes depend on build/register path in `src/server.ts` / `src/proxy/*`. +Exact routes depend on the modules registered in `src/server.ts` and +`src/proxy/`. ## Configuration pattern -1. Configure upstream base URLs via env (`*_UPSTREAM_URL`) when you need overrides. -2. Add provider credentials through admin/secure config — not plaintext git files. -3. Map gateway models to upstream models via your model catalog / access rules. +1. Start the gateway and bootstrap the dashboard. +2. Add an upstream provider account or credential through the dashboard/admin + API. Do not place live credentials in tracked files. +3. Configure optional `*_UPSTREAM_URL` overrides only when required. +4. Issue a gateway API token to the calling user. +5. Verify a low-cost request with a model available to that user. -## Pools & governor +## Pools and concurrency controls -`src/providers/*-pool.ts` and `governor.ts` implement account selection, concurrency, and health-aware routing. Self-host deployments can run with a single upstream key or multiple pooled accounts. +Modules under `src/providers/` implement account selection, concurrency, and +health-aware routing. A self-host deployment may use one upstream account or a +pool, subject to each provider's terms. ## Fusion -Fusion (multi-model panel + synthesizer) lives under `src/fusion/*` and `/v1` fusion routes when enabled. Treat it as an advanced feature; basic proxying does not require it. +Fusion combines multiple model calls and a synthesizer. It is an advanced +feature; basic proxying does not require it. Operators are responsible for the +cost and data-handling implications of sending one prompt to multiple +providers. -## Adding a provider (contributors) +## Adding a provider -1. Add pool + proxy route module. -2. Register in server bootstrap. -3. Add focused tests with Fastify inject. -4. Document env vars in `.env.example` and this file. +1. Add the provider pool and proxy route module. +2. Register the route during server startup. +3. Add focused tests with Fastify injection and mocked upstream traffic. +4. Document environment variables in `.env.example` and this file. +5. Update the public model catalog and client documentation where applicable. diff --git a/package-lock.json b/package-lock.json index 2319214..99f2948 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,13 @@ { - "name": "model-gateway", + "name": "super-proxy", "version": "0.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "model-gateway", + "name": "super-proxy", "version": "0.1.0", + "license": "Apache-2.0", "dependencies": { "@fastify/cookie": "11.0.2", "@fastify/cors": "11.2.0", @@ -28,6 +29,9 @@ "@types/ws": "8.18.1", "tsx": "4.21.0", "typescript": "6.0.3" + }, + "engines": { + "node": ">=20" } }, "node_modules/@esbuild/aix-ppc64": { @@ -572,9 +576,9 @@ "license": "MIT" }, "node_modules/@fastify/fast-json-stringify-compiler": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/@fastify/fast-json-stringify-compiler/-/fast-json-stringify-compiler-5.0.3.tgz", - "integrity": "sha512-uik7yYHkLr6fxd8hJSZ8c+xF4WafPK+XzneQDPU+D10r5X19GW8lJcom2YijX2+qtFF1ENJlHXKFM9ouXNJYgQ==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@fastify/fast-json-stringify-compiler/-/fast-json-stringify-compiler-5.1.0.tgz", + "integrity": "sha512-PxcYtKLbQ8Z+yApiqjK8FwxIwvEj38k2OiLc17u8dkJSlmfi2wHHPaSnaoqBPQqtvF8YVsDgDpP2snDCfFrpfw==", "funding": [ { "type": "github", @@ -587,9 +591,49 @@ ], "license": "MIT", "dependencies": { - "fast-json-stringify": "^6.0.0" + "fast-json-stringify": "^7.0.0" } }, + "node_modules/@fastify/fast-json-stringify-compiler/node_modules/fast-json-stringify": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fast-json-stringify/-/fast-json-stringify-7.0.1.tgz", + "integrity": "sha512-eRSayARSbbwlBjpP4vnTTIRD5QPcIrmihPxDeN1DtKnHPg66UuJLx+8hlK1kaFdjvzyQ/dzALoi4vwAQ+T+iZA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/merge-json-schemas": "^0.2.0", + "ajv": "^8.12.0", + "ajv-formats": "^3.0.1", + "fast-uri": "^4.0.0", + "json-schema-ref-resolver": "^3.0.0", + "rfdc": "^1.2.0" + } + }, + "node_modules/@fastify/fast-json-stringify-compiler/node_modules/fast-uri": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-4.1.1.tgz", + "integrity": "sha512-YPOs1zD5TG2+EZt+r88LwF6mclA7TPkpwMP7ZN3TO2HiHS8TXvq7QA/17iJsV9dubcLo/f8eEYqMBruyQV21hQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/@fastify/forwarded": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@fastify/forwarded/-/forwarded-3.0.1.tgz", @@ -714,33 +758,33 @@ } }, "node_modules/@firebase/app-check-interop-types": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@firebase/app-check-interop-types/-/app-check-interop-types-0.3.3.tgz", - "integrity": "sha512-gAlxfPLT2j8bTI/qfe3ahl2I2YcBQ8cFIBdhAQA4I2f3TndcO+22YizyGYuttLHPQEpWkhmpFW60VCFEPg4g5A==", + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@firebase/app-check-interop-types/-/app-check-interop-types-0.3.4.tgz", + "integrity": "sha512-zz3i6e13B8BfWiLy8MABtTh8aGIACgKbf9UVnyHcWs+yQzJXgQcl8A46b0zfaiJHdQ+niF0ouAfcpuf+3LMPQg==", "license": "Apache-2.0" }, "node_modules/@firebase/app-types": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.9.4.tgz", - "integrity": "sha512-crX9TA5SVYZwLPG7/R16IsH8FLlgkPXjJUVhsVpHVDSqJiq3D/NuFTM5ctxGTExXAOeIn//69tQw47CPerM8MQ==", + "version": "0.9.5", + "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.9.5.tgz", + "integrity": "sha512-YevqTjvo7Iujsa9Dwowmd6dSoElhzmD63ZSrq6bzjvQ6POjYgNjOFHLmNIgJs48eNO093NCERibuFnxbfOvU7A==", "license": "Apache-2.0", "dependencies": { - "@firebase/logger": "0.5.0" + "@firebase/logger": "0.5.1" } }, "node_modules/@firebase/auth-interop-types": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.2.4.tgz", - "integrity": "sha512-JPgcXKCuO+CWqGDnigBtvo09HeBs5u/Ktc2GaFj2m01hLarbxthLNm7Fk8iOP1aqAtXV+fnnGj7U28xmk7IwVA==", + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.2.5.tgz", + "integrity": "sha512-1Li/YuBDBAXcKv7BzY4U28gontUmAaw53sYiqbaVOMCFb2lFKK/c3CGMUWqtwe7+TXrl3poWnTCL5umYBg85Eg==", "license": "Apache-2.0" }, "node_modules/@firebase/component": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.7.2.tgz", - "integrity": "sha512-iyVDGc6Vjx7Rm0cAdccLH/NG6fADsgJak/XW9IA2lPf8AjIlsemOpFGKczYyPHxm4rnKdR8z6sK4+KEC7NwmEg==", + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.7.3.tgz", + "integrity": "sha512-wFofIaa2879ogD/WvkjYXJxRmfnL0scen6ORgaC3na1FNOR9ASIUANQdhqQcmWu/h77/pVHY7ch5flewa5Bcew==", "license": "Apache-2.0", "dependencies": { - "@firebase/util": "1.15.0", + "@firebase/util": "1.15.1", "tslib": "^2.1.0" }, "engines": { @@ -748,16 +792,16 @@ } }, "node_modules/@firebase/database": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@firebase/database/-/database-1.1.2.tgz", - "integrity": "sha512-lP96CMjMPy/+d1d9qaaHjHHdzdwvEOuyyLq9ehX89e2XMKwS1jHNzYBO+42bdSumuj5ukPbmnFtViZu8YOMT+w==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@firebase/database/-/database-1.1.3.tgz", + "integrity": "sha512-XwWCa+E4TvNGpGwXrycLRNfdogADwFcvuhyow6wDWma9W54roaQIhe+4PM0KiLsIftBdSCGI7OKCXrdSRHbIhw==", "license": "Apache-2.0", "dependencies": { - "@firebase/app-check-interop-types": "0.3.3", - "@firebase/auth-interop-types": "0.2.4", - "@firebase/component": "0.7.2", - "@firebase/logger": "0.5.0", - "@firebase/util": "1.15.0", + "@firebase/app-check-interop-types": "0.3.4", + "@firebase/auth-interop-types": "0.2.5", + "@firebase/component": "0.7.3", + "@firebase/logger": "0.5.1", + "@firebase/util": "1.15.1", "faye-websocket": "0.11.4", "tslib": "^2.1.0" }, @@ -766,16 +810,16 @@ } }, "node_modules/@firebase/database-compat": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-2.1.3.tgz", - "integrity": "sha512-GMyfWjD8mehjg/QpNkY/tl9G/MoeugPeg91n9D0atggxbWuKF/2KhVPHZDH+XmoP0EKYqMWYTtKxBsaBaNKLYQ==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-2.1.4.tgz", + "integrity": "sha512-3pK35F1MAgmqFJQlf2nhQl44vtAXQO1uaCaQOEUI9kCRtLFqi7N+QRKR7lFZPg+xIZIyubgxQaxY69YgfZRZWg==", "license": "Apache-2.0", "dependencies": { - "@firebase/component": "0.7.2", - "@firebase/database": "1.1.2", - "@firebase/database-types": "1.0.19", - "@firebase/logger": "0.5.0", - "@firebase/util": "1.15.0", + "@firebase/component": "0.7.3", + "@firebase/database": "1.1.3", + "@firebase/database-types": "1.0.20", + "@firebase/logger": "0.5.1", + "@firebase/util": "1.15.1", "tslib": "^2.1.0" }, "engines": { @@ -783,19 +827,19 @@ } }, "node_modules/@firebase/database-types": { - "version": "1.0.19", - "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-1.0.19.tgz", - "integrity": "sha512-FqewjUZmV9LqFfuEnmgdcUpiOUz7qwLXxnm/H8BcMFEzQXtd1yyUDm8ex5VRad2nuTE+ahOuCjUAM/cyDncO+g==", + "version": "1.0.20", + "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-1.0.20.tgz", + "integrity": "sha512-kegbOk/w8iU64pr0q6k2ItyNGjnQBMHFhwS7ohdWI4W+pc0/zhhdGXTdFj6X1oxItRjPoYOsSQmERgBkn/ihxw==", "license": "Apache-2.0", "dependencies": { - "@firebase/app-types": "0.9.4", - "@firebase/util": "1.15.0" + "@firebase/app-types": "0.9.5", + "@firebase/util": "1.15.1" } }, "node_modules/@firebase/logger": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.5.0.tgz", - "integrity": "sha512-cGskaAvkrnh42b3BA3doDWeBmuHFO/Mx5A83rbRDYakPjO9bJtRL3dX7javzc2Rr/JHZf4HlterTW2lUkfeN4g==", + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.5.1.tgz", + "integrity": "sha512-vZKLsqE1ABOy8OjQiE7cUTFn4gvaqlk88yp8N94Pk/sDpq61YqZGqmVFZTvOyflTwuYFcWirBdYGoJgbDaXKYQ==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.1.0" @@ -805,9 +849,9 @@ } }, "node_modules/@firebase/util": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.15.0.tgz", - "integrity": "sha512-AmWf3cHAOMbrCPG4xdPKQaj5iHnyYfyLKZxwz+Xf55bqKbpAmcYifB4jQinT2W9XhDRHISOoPyBOariJpCG6FA==", + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.15.1.tgz", + "integrity": "sha512-LUdM4Wg7YM9Pq/49nGYySJA0CSQEKnGffFzWV8+6gXN7mGxn+FL1IqvFbuZUtAQcfZgHYDwCE1wwlK7rB7gl2g==", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { @@ -869,9 +913,9 @@ } }, "node_modules/@google-cloud/storage": { - "version": "7.19.0", - "resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-7.19.0.tgz", - "integrity": "sha512-n2FjE7NAOYyshogdc7KQOl/VZb4sneqPjWouSyia9CMDdMhRX5+RIbqalNmC7LOLzuLAN89VlF2HvG8na9G+zQ==", + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-7.21.0.tgz", + "integrity": "sha512-l+IFTkd+6Y5LoAuXyYCKNAKtw/Ci+rAMqgdTB1jv4iZiLhw0rtq+0qjIRbBizXkNzEFmXiXUW0H7sZQQvk1ffA==", "license": "Apache-2.0", "optional": true, "dependencies": { @@ -888,8 +932,7 @@ "mime": "^3.0.0", "p-limit": "^3.0.1", "retry-request": "^7.0.0", - "teeny-request": "^9.0.0", - "uuid": "^8.0.0" + "teeny-request": "^9.0.0" }, "engines": { "node": ">=14" @@ -938,21 +981,10 @@ "node": ">=14" } }, - "node_modules/@google-cloud/storage/node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", - "license": "MIT", - "optional": true, - "bin": { - "uuid": "dist/bin/uuid" - } - }, "node_modules/@grpc/grpc-js": { - "version": "1.14.3", - "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.3.tgz", - "integrity": "sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA==", + "version": "1.14.4", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.4.tgz", + "integrity": "sha512-k9Dj3DV/itK9D06Y8f190Qgop7/Ui+D0njFV3LHMPwPT75DpXLQohE9Wmz0QElrJnzsjB7KPWiKJbOl7IPDArQ==", "license": "Apache-2.0", "optional": true, "dependencies": { @@ -964,15 +996,15 @@ } }, "node_modules/@grpc/grpc-js/node_modules/@grpc/proto-loader": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.8.0.tgz", - "integrity": "sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ==", + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.8.1.tgz", + "integrity": "sha512-wtF6h+DY6M3YaDBPAmvuuA6jV8Sif9MjtOI5euKFWRgCDl5PeDpPsHR9u2l6St5ceY8AZgoNDww5+HvEsXFsGg==", "license": "Apache-2.0", "optional": true, "dependencies": { "lodash.camelcase": "^4.3.0", "long": "^5.0.0", - "protobufjs": "^7.5.3", + "protobufjs": "^7.5.5", "yargs": "^17.7.2" }, "bin": { @@ -1022,9 +1054,9 @@ } }, "node_modules/@nodable/entities": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.1.0.tgz", - "integrity": "sha512-nyT7T3nbMyBI/lvr6L5TyWbFJAI9FTgVRakNoBqCD+PmID8DzFrrNdLLtHMwMszOtqZa8PAOV24ZqDnQrhQINA==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-3.0.0.tgz", + "integrity": "sha512-8L9xFeTYKhm49xfIypoe2W5wV1m/3Z58kT+7kR9A8OyFxcPduI4VmxaUMQyKYrRjUoLLSXv6EKKID5Tvj9cUVw==", "funding": [ { "type": "github", @@ -1072,21 +1104,20 @@ "optional": true }, "node_modules/@protobufjs/eventemitter": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", - "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz", + "integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==", "license": "BSD-3-Clause", "optional": true }, "node_modules/@protobufjs/fetch": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", - "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz", + "integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==", "license": "BSD-3-Clause", "optional": true, "dependencies": { - "@protobufjs/aspromise": "^1.1.1", - "@protobufjs/inquire": "^1.1.0" + "@protobufjs/aspromise": "^1.1.1" } }, "node_modules/@protobufjs/float": { @@ -1096,13 +1127,6 @@ "license": "BSD-3-Clause", "optional": true }, - "node_modules/@protobufjs/inquire": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.1.tgz", - "integrity": "sha512-mnzgDV26ueAvk7rsbt9L7bE0SuAoqyuys/sMMrmVcN5x9VsxpcG3rqAUSgDyLp0UZlmNfIbQ4fHfCtreVBk8Ew==", - "license": "BSD-3-Clause", - "optional": true - }, "node_modules/@protobufjs/path": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", @@ -1118,9 +1142,9 @@ "optional": true }, "node_modules/@protobufjs/utf8": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.1.tgz", - "integrity": "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.2.tgz", + "integrity": "sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug==", "license": "BSD-3-Clause", "optional": true }, @@ -1300,6 +1324,19 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/anynum": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/anynum/-/anynum-1.0.1.tgz", + "integrity": "sha512-N6//FLET/tXYNM/F6ABca1oH6fWB+KlTt909Le28WMDBk8oaT4vY17DCrwg2MvmuqUKt3Ni4N5dGJ/EoBgcO6A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "optional": true + }, "node_modules/arrify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", @@ -1337,9 +1374,9 @@ } }, "node_modules/avvio": { - "version": "9.2.0", - "resolved": "https://registry.npmjs.org/avvio/-/avvio-9.2.0.tgz", - "integrity": "sha512-2t/sy01ArdHHE0vRH5Hsay+RtCZt3dLPji7W7/MMOCEgze5b7SNDC4j5H6FnVgPkI1MTNFGzHdHrVXDDl7QSSQ==", + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/avvio/-/avvio-9.3.0.tgz", + "integrity": "sha512-g2tQ7LE7oOSqDfwEm3M+ZCMTJc7KiZCdJ4UwyZJb5ckTKyYu50OYmvv0mCFXPuYXoM4zkSt8zM9XQ9KCvxA74A==", "funding": [ { "type": "github", @@ -1429,9 +1466,9 @@ } }, "node_modules/brace-expansion": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", - "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", + "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" @@ -1736,9 +1773,9 @@ } }, "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", "license": "MIT", "optional": true, "dependencies": { @@ -1902,9 +1939,9 @@ } }, "node_modules/fast-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", - "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.4.tgz", + "integrity": "sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw==", "funding": [ { "type": "github", @@ -1918,9 +1955,9 @@ "license": "BSD-3-Clause" }, "node_modules/fast-xml-builder": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.9.tgz", - "integrity": "sha512-jcyKVSEX13iseJqg7n/KWw+xnu/7fdrZ333Fac54KjHDIELVCfDDJXYIm6DTJ0Su4gSzrhqiK0DzY/wZbF40mw==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.3.0.tgz", + "integrity": "sha512-F74cZEdCvuw9P41GAC3rod4X04jjWGM1JPEv/GWSqFTWLsdyMSBMBMlm9Hk3GLBgLBbdBNY8yee0pQh2RBVESQ==", "funding": [ { "type": "github", @@ -1930,13 +1967,14 @@ "license": "MIT", "optional": true, "dependencies": { - "path-expression-matcher": "^1.1.3" + "path-expression-matcher": "^1.6.2", + "xml-naming": "^0.3.0" } }, "node_modules/fast-xml-parser": { - "version": "5.7.3", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.7.3.tgz", - "integrity": "sha512-C0AaNuC+mscy6vrAQKAc/rMq+zAPHodfHGZu4sGVehvAQt/JLG1O5zEcYcXSY5zSqr4YVgxsB+pHXTq0i7eDlg==", + "version": "5.10.1", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.10.1.tgz", + "integrity": "sha512-IEMIf7298kXuZSRFoGfMYrl7is8LpavODgbNz1cwIudv7KwVFnuU+UsMporfq6PD6aXSlawZlARiA3UywCTfMw==", "funding": [ { "type": "github", @@ -1946,10 +1984,12 @@ "license": "MIT", "optional": true, "dependencies": { - "@nodable/entities": "^2.1.0", - "fast-xml-builder": "^1.1.7", - "path-expression-matcher": "^1.5.0", - "strnum": "^2.2.3" + "@nodable/entities": "^3.0.0", + "fast-xml-builder": "^1.2.0", + "is-unsafe": "^2.0.0", + "path-expression-matcher": "^1.6.2", + "strnum": "^2.4.1", + "xml-naming": "^0.3.0" }, "bin": { "fxparser": "src/cli/cli.js" @@ -2094,16 +2134,16 @@ } }, "node_modules/form-data": { - "version": "2.5.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.5.tgz", - "integrity": "sha512-jqdObeR2rxZZbPSGL+3VckHMYtu+f9//KXBsVny6JSX/pa38Fy+bGjuG8eW/H6USNQWhLi8Num++cU2yOCNz4A==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.6.tgz", + "integrity": "sha512-Ogz/E85h9tlfJzpI6TuFpGcHZFhLrb9Gw8wq9v40CxSCPnv7ahKr6Xgtkn0KYCDQJ8DNn5VoMO8EXr9V5PadyA==", "license": "MIT", "optional": true, "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", + "hasown": "^2.0.4", "mime-types": "^2.1.35", "safe-buffer": "^5.2.1" }, @@ -2208,9 +2248,9 @@ } }, "node_modules/gcp-metadata/node_modules/gaxios": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.4.tgz", - "integrity": "sha512-bTIgTsM2bWn3XklZISBTQX7ZSddGW+IO3bMdGaemHZ3tbqExMENHLx6kKZ/KlejgrMtj8q7wBItt51yegqalrA==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.2.0.tgz", + "integrity": "sha512-CUVb4wcYe+771XevyH6HtGmXFAGGKkIC3kswAP8Z1JCe0j80JMaTPZH930DWFrvo0atjh18Arc0pEyUCWa5bfg==", "license": "Apache-2.0", "dependencies": { "extend": "^3.0.2", @@ -2325,9 +2365,9 @@ } }, "node_modules/google-auth-library": { - "version": "10.6.2", - "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.6.2.tgz", - "integrity": "sha512-e27Z6EThmVNNvtYASwQxose/G57rkRuaRbQyxM2bvYLLX/GqWZ5chWq2EBoUchJbCc57eC9ArzO5wMsEmWftCw==", + "version": "10.9.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.9.0.tgz", + "integrity": "sha512-xtvUqvINPhTaBm7nXqlYPcrMHJPm1lCNdSovxnKKhTm+4JsvQ+KGVYJViLoH9Yxu8w+T0Qv5HubzYT9BLrppJg==", "license": "Apache-2.0", "dependencies": { "base64-js": "^1.3.0", @@ -2342,9 +2382,9 @@ } }, "node_modules/google-auth-library/node_modules/gaxios": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.4.tgz", - "integrity": "sha512-bTIgTsM2bWn3XklZISBTQX7ZSddGW+IO3bMdGaemHZ3tbqExMENHLx6kKZ/KlejgrMtj8q7wBItt51yegqalrA==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.2.0.tgz", + "integrity": "sha512-CUVb4wcYe+771XevyH6HtGmXFAGGKkIC3kswAP8Z1JCe0j80JMaTPZH930DWFrvo0atjh18Arc0pEyUCWa5bfg==", "license": "Apache-2.0", "dependencies": { "extend": "^3.0.2", @@ -2521,9 +2561,9 @@ } }, "node_modules/hasown": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", - "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", "license": "MIT", "optional": true, "dependencies": { @@ -2681,6 +2721,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-unsafe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-unsafe/-/is-unsafe-2.0.0.tgz", + "integrity": "sha512-2LdV822R+wmI86unXA93WCFpL6g+av8ynWk0nrHyJqGop5VoocYsSLFgN8jrfalT6iGeLNM4KXuVSsULP53kEA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "optional": true + }, "node_modules/jose": { "version": "4.15.9", "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz", @@ -3036,9 +3089,9 @@ "license": "MIT" }, "node_modules/node-abi": { - "version": "3.91.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.91.0.tgz", - "integrity": "sha512-B+S7X/GS3Un6wMICtnsNjQD7oSpVBQrZftHE6GZ1Fe9/k3XOOoqbM5DZZ0GO4x3YiSCQfrM28yj1ppplwgIsfg==", + "version": "3.94.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.94.0.tgz", + "integrity": "sha512-W5ZNO5KRPB5TkYmGVD9F6YqhsglXJzE6etpbmT+f6EQElhiX/UTG551cnsRGvLG3fyZEg9HwaDmNmj5nwJ4z9g==", "license": "MIT", "dependencies": { "semver": "^7.3.5" @@ -3142,9 +3195,9 @@ } }, "node_modules/path-expression-matcher": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.5.0.tgz", - "integrity": "sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==", + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.6.2.tgz", + "integrity": "sha512-enSlaiat05iasnzmgNxRj8reFdj3puY2QpNgP1aPIaVfT6nn9ICuPoFlKHk8EN22HcwewshO+mN2DGbkCEOtqQ==", "funding": [ { "type": "github", @@ -3174,9 +3227,9 @@ } }, "node_modules/path-scurry/node_modules/lru-cache": { - "version": "11.3.6", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.6.tgz", - "integrity": "sha512-Gf/KoL3C/MlI7Bt0PGI9I+TeTC/I6r/csU58N4BSNc4lppLBeKsOdFYkK+dX0ABDUMJNfCHTyPpzwwO21Awd3A==", + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", "license": "BlueOak-1.0.0", "engines": { "node": "20 || >=22" @@ -3276,9 +3329,9 @@ } }, "node_modules/protobufjs": { - "version": "7.5.6", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.6.tgz", - "integrity": "sha512-M71sTMB146U3u0di3yup8iM+zv8yPRNQVr1KK4tyBitl3qFvEGucq/rGDRShD2rsJhtN02RJaJ7j5X5hmy8SJg==", + "version": "7.6.5", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.5.tgz", + "integrity": "sha512-/FPD0nUc9jH6rfFjji9IBqOz4pcSE3CsT1m7Ep6Mdb0LxSUMj8hgl6GomOvZzpNpAqqGaXA0P3VSrZLFzIhQrw==", "hasInstallScript": true, "license": "BSD-3-Clause", "optional": true, @@ -3286,15 +3339,14 @@ "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.5", - "@protobufjs/eventemitter": "^1.1.0", - "@protobufjs/fetch": "^1.1.0", + "@protobufjs/eventemitter": "^1.1.1", + "@protobufjs/fetch": "^1.1.1", "@protobufjs/float": "^1.0.2", - "@protobufjs/inquire": "^1.1.1", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.1", "@types/node": ">=13.7.0", - "long": "^5.0.0" + "long": "^5.3.2" }, "engines": { "node": ">=12.0.0" @@ -3501,9 +3553,9 @@ "license": "BSD-3-Clause" }, "node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -3659,9 +3711,9 @@ } }, "node_modules/strnum": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.3.tgz", - "integrity": "sha512-oKx6RUCuHfT3oyVjtnrmn19H1SiCqgJSg+54XqURKp5aCMbrXrhLjRN9TjuwMjiYstZ0MzDrHqkGZ5dFTKd+zg==", + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.4.1.tgz", + "integrity": "sha512-M9eUSMT2dCB2cTNPG7UYj6KuK7RJR2SN2+yCV/fTW3xzTCS6EaGZ5pSMgDIjB7r8zSfTGk+dvvn9rTjpVS9Mwg==", "funding": [ { "type": "github", @@ -3669,7 +3721,10 @@ } ], "license": "MIT", - "optional": true + "optional": true, + "dependencies": { + "anynum": "^1.0.1" + } }, "node_modules/stubs": { "version": "3.0.0", @@ -3679,9 +3734,9 @@ "optional": true }, "node_modules/tar-fs": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", - "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.5.tgz", + "integrity": "sha512-OboTd8mmMhZDNPV+UjQcK9yKAatXu2aJ+r1w4im1Otd4M4fl2hwvdoXUxIYHFTHWK/3y3FarBP70v3vwmGlOxw==", "license": "MIT", "dependencies": { "chownr": "^1.1.1", @@ -3766,24 +3821,30 @@ } }, "node_modules/thread-stream": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-4.0.0.tgz", - "integrity": "sha512-4iMVL6HAINXWf1ZKZjIPcz5wYaOdPhtO8ATvZ+Xqp3BTdaqtAwQkNmKORqcIo5YkQqGXq5cwfswDwMqqQNrpJA==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-4.2.0.tgz", + "integrity": "sha512-e2zZ96wSChazBsbENf/Pcm/4swHt2cEKQ92rhUjkL9GCKiTDJIaTBenjE/m9DXi0QBmTMDkFDdOomUy20A1tDQ==", "license": "MIT", "dependencies": { - "real-require": "^0.2.0" + "real-require": "^1.0.0" }, "engines": { "node": ">=20" } }, + "node_modules/thread-stream/node_modules/real-require": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-1.0.0.tgz", + "integrity": "sha512-P4nbQYQfePJxRSmY+v/KINxVucm4NF3p3s7pJveMTtom52FR4YGltUQLB8idDXwDDWW+eYrWDFbuzUnjoWHF7g==", + "license": "MIT" + }, "node_modules/toad-cache": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/toad-cache/-/toad-cache-3.7.0.tgz", - "integrity": "sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==", + "version": "3.7.4", + "resolved": "https://registry.npmjs.org/toad-cache/-/toad-cache-3.7.4.tgz", + "integrity": "sha512-m1TdR/rvT7kgGJZhspNtXdsdYk0fddFpJJFlG5s+UkPFo6lkLoZ3YLOaovPYjq1R75NP5JfeTlSHaOsE09peCg==", "license": "MIT", "engines": { - "node": ">=12" + "node": ">=20" } }, "node_modules/toidentifier": { @@ -3905,9 +3966,9 @@ "optional": true }, "node_modules/websocket-driver": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", - "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.5.tgz", + "integrity": "sha512-ZL2+3c7kMBdIRCMz6l8jQMHyGVxj+UL+xVk74Ombiciboca8rHa15L86B19E5oh1pL9Ii/uj54gtsIrZGMo6zA==", "license": "Apache-2.0", "dependencies": { "http-parser-js": ">=0.5.1", @@ -3983,6 +4044,22 @@ } } }, + "node_modules/xml-naming": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.3.0.tgz", + "integrity": "sha512-ghig2TBE/H11aOVgmahA3MhimvkBr6JIYknH/Dhdk10nXwdbIqBJsbfMxpvFPG8bAw77gN29aQWvKpmVoPlvPQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "optional": true, + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -4000,9 +4077,9 @@ "license": "ISC" }, "node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "version": "17.7.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", "license": "MIT", "optional": true, "dependencies": { diff --git a/package.json b/package.json index 5177cff..b30bcba 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,19 @@ "version": "0.1.0", "private": true, "type": "module", + "description": "Open-source multi-provider AI gateway with OpenAI- and Anthropic-compatible APIs, usage controls, and a self-hosted dashboard", + "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "git+https://github.com/Nextbasedev/super-proxy.git" + }, + "homepage": "https://github.com/Nextbasedev/super-proxy#readme", + "bugs": { + "url": "https://github.com/Nextbasedev/super-proxy/issues" + }, + "engines": { + "node": ">=20" + }, "scripts": { "dev": "tsx watch src/server.ts", "start": "node dist/server.js", @@ -10,6 +23,16 @@ "test": "node scripts/run-tests.mjs", "db:migrate": "tsx src/db/migrate.ts" }, + "keywords": [ + "ai", + "llm", + "gateway", + "proxy", + "openai", + "anthropic", + "streaming", + "self-hosted" + ], "dependencies": { "@fastify/cookie": "11.0.2", "@fastify/cors": "11.2.0", @@ -31,24 +54,5 @@ "@types/ws": "8.18.1", "tsx": "4.21.0", "typescript": "6.0.3" - }, - "description": "Super Proxy \u2014 open-source multi-provider AI gateway with OpenAI & Anthropic-compatible APIs, usage controls, and a self-host dashboard", - "license": "Apache-2.0", - "engines": { - "node": ">=20" - }, - "repository": { - "type": "git", - "url": "https://github.com/example/super-proxy.git" - }, - "keywords": [ - "ai", - "llm", - "gateway", - "proxy", - "openai", - "anthropic", - "streaming", - "self-hosted" - ] + } } diff --git a/public/SEARCH.md b/public/SEARCH.md deleted file mode 100644 index ac8215b..0000000 --- a/public/SEARCH.md +++ /dev/null @@ -1,288 +0,0 @@ -# Super Proxy Search API - -Use `POST /v1/search` when you want a simple web-search endpoint through the Super Proxy proxy. - -This endpoint is authenticated with the same `sp_*` proxy token used for all other gateway routes. - -## Base URL - -```text -http://localhost:8080 -``` - -## Authentication - -Any of these token styles work: - -```http -Authorization: Bearer sp_xxx -``` - -```http -x-api-key: sp_xxx -``` - -```http -api-key: sp_xxx -``` - -```http -apikey: sp_xxx -``` - -Do not send upstream provider keys. Send only your Super Proxy gateway token. - -## Quick start - -```bash -curl "http://localhost:8080/v1/search" \ - -H "Authorization: Bearer $NEXTBASE_API_KEY" \ - -H "Content-Type: application/json" \ - -d '{ - "query": "latest AI news today" - }' -``` - -Minimum request: - -```json -{ - "query": "latest AI news today" -} -``` - -`query` is the only required parameter. Everything else is optional. - -## Request body - -```json -{ - "query": "latest AI news today", - "mode": "models", - "provider": "gemini", - "model": "gemini-2.5-flash-lite", - "limit": 10, - "max_tokens": 768 -} -``` - -### Parameters - -| Field | Required | Default | Description | -| --- | --- | --- | --- | -| `query` | Yes | — | Search query. | -| `mode` | No | `models` | `models` for model-backed search, `serp` for paid Google SERP via Serper. | -| `provider` | No | `gemini` | Model-search provider: `gemini`, `xai`, or `codex`. | -| `model` | No | Provider default | Override the model used for model-backed search. | -| `limit` | No | `10` | Max indexed results to return. Clamped between `1` and `20`. | -| `max_tokens` | No | `768` | Output budget for Gemini search. | -| `gl` / `country` | No | — | Serper Google country code, e.g. `us`, `in`, `lu`. | -| `hl` / `language` | No | — | Serper Google language code, e.g. `en`, `hi`. | -| `tbs` / `dateRange` / `date_range` | No | — | Serper/Google date range filter, e.g. `qdr:d`, `qdr:w`, `qdr:m`, `qdr:y`. | -| `page` | No | — | Serper page number for pagination. | -| `max_output_tokens` | No | `768` | Output budget for xAI/Codex search. | - -## Provider defaults - -| Provider | Default model | Backend route | -| --- | --- | --- | -| `gemini` | `gemini-2.5-flash-lite` | `/v1/gemini/chat/completions` with Google Search grounding | -| `xai` | `grok-4.3` | `/v1/xai/responses` with Agent Tools `web_search` | -| `codex` | `gpt-5.4-mini` | `/v1/responses` with `web_search` | - -## Response - -```json -{ - "query": "latest AI news today", - "mode": "models", - "provider": "gemini", - "model": "gemini-2.5-flash-lite", - "answer": "Concise model-generated answer...", - "results": [ - { - "index": 1, - "title": "Source title or domain", - "url": "https://example.com/article", - "source": "grounding" - } - ], - "usage": { - "prompt_tokens": 28, - "completion_tokens": 558, - "total_tokens": 657, - "tool_use_prompt_tokens": 71 - }, - "grounding_metadata": {} -} -``` - -### Response fields - -| Field | Description | -| --- | --- | -| `query` | The query you sent. | -| `mode` | Active mode, currently `models`. | -| `provider` | Provider used for search. | -| `model` | Model used for search. | -| `answer` | Model-generated answer using web search/grounding. | -| `results` | Indexed source list extracted from provider citations/grounding. | -| `results[].index` | Stable 1-based result number. | -| `results[].title` | Source title/domain when available. | -| `results[].url` | Source URL. Gemini may return Google grounding redirect URLs. | -| `results[].source` | Source type, e.g. `grounding` or `annotation`. | -| `usage` | Token/tool usage returned by the model provider. | -| `grounding_metadata` | Gemini raw grounding metadata when provider is `gemini`. | - -## Examples - -### Default Gemini search - -```bash -curl "$BASE_URL/v1/search" \ - -H "x-api-key: $NEXTBASE_API_KEY" \ - -H "Content-Type: application/json" \ - -d '{"query":"what happened in AI today?","limit":5}' -``` - -### Paid Google SERP via Serper - -```bash -curl "$BASE_URL/v1/search" \ - -H "x-api-key: $NEXTBASE_API_KEY" \ - -H "Content-Type: application/json" \ - -d '{ - "query":"apple inc", - "mode":"serp", - "limit":10, - "country":"us", - "language":"en", - "tbs":"qdr:d", - "page":1 - }' -``` - -### xAI search - -```bash -curl "$BASE_URL/v1/search" \ - -H "x-api-key: $NEXTBASE_API_KEY" \ - -H "Content-Type: application/json" \ - -d '{ - "query":"latest SpaceX news", - "provider":"xai", - "model":"grok-4.3", - "limit":5 - }' -``` - -### Codex/OpenAI Responses search - -```bash -curl "$BASE_URL/v1/search" \ - -H "x-api-key: $NEXTBASE_API_KEY" \ - -H "Content-Type: application/json" \ - -d '{ - "query":"latest OpenAI product updates", - "provider":"codex", - "model":"gpt-5.4-mini", - "limit":5 - }' -``` - -## Modes - -### `mode: "models"` - -Returns model-backed search results, not raw Google SERP rankings. - -That means: - -- `gemini` uses Google Search grounding and citation metadata. -- `xai` uses xAI Agent Tools web search. -- `codex` uses OpenAI/Codex web search. -- Result order comes from provider citations/grounding, not guaranteed Google organic SERP order. - -### `mode: "serp"` - -Returns fresh paid Google SERP results through Serper. No cache is used. - -```json -{ - "query": "latest AI news today", - "mode": "serp", - "limit": 10, - "country": "us", - "language": "en", - "tbs": "qdr:d", - "page": 1 -} -``` - -SERP results include classic `index`, `title`, `url`, `display_url`, and `snippet` fields. The backend is Serper today; the response shape is provider-agnostic so it can later move to ClawSearch if needed. - -## Errors - -Missing query: - -```json -{ - "error": { - "type": "invalid_request_error", - "message": "query is required" - } -} -``` - -Unsupported provider: - -```json -{ - "error": { - "type": "invalid_request_error", - "message": "provider must be one of: gemini, xai, codex" - } -} -``` - -Unsupported mode: - -```json -{ - "error": { - "type": "invalid_request_error", - "message": "mode must be one of: models, serp" - } -} -``` - -## Recommended default - -Use this unless you have a specific reason to choose another provider: - -```json -{ - "query": "your search query", - "provider": "gemini", - "limit": 10 -} -``` - -Gemini is the best default today because it has native Google Search grounding and returns structured grounding metadata. - - -## Serper parameter aliases - -For `mode: "serp"`, Super Proxy accepts both friendly and native Serper names: - -- `country` or `gl` -- `language` or `hl` -- `tbs`, `dateRange`, or `date_range` -- `page` - -Examples for `tbs`: - -- `qdr:d` — past day -- `qdr:w` — past week -- `qdr:m` — past month -- `qdr:y` — past year diff --git a/public/console.js b/public/console.js index b12db1c..811aa6b 100644 --- a/public/console.js +++ b/public/console.js @@ -166,7 +166,7 @@ const fmt = { }; // Render audit target_id consistently. Numeric ids (users, tokens, accounts) // get a leading '#' so they read as 'user #4'. String ids (e.g. provider OAuth -// flow keys like 'nextbase.paradox') are rendered verbatim, no fake ID glyph. +// flow keys like 'provider.example') are rendered verbatim, no fake ID glyph. function formatAuditTargetId(targetType, targetId) { if (targetId == null) return ''; const s = String(targetId); @@ -197,7 +197,6 @@ const ROUTES = { // dev home: { title: 'Overview', subtitle: 'Your access, limits and recent activity', adminOnly: false, render: () => renderHome() }, spend: { title: 'Spend', subtitle: 'Your usage broken down by provider and model', adminOnly: false, render: () => renderSpend() }, - setup: { title: 'Setup', subtitle: 'Wire OpenClaw (and other clients) to this gateway', adminOnly: false, render: () => renderSetup() }, fusion: { title: 'Fusion', subtitle: 'Multi-model presets and call history', adminOnly: false, render: () => renderFusion() }, monitoring: { title: 'Monitoring', subtitle: 'Cost, cache efficiency, pool health and reliability', adminOnly: false, render: () => renderMonitoring() }, @@ -221,13 +220,11 @@ const NAV_ADMIN = [ { route: 'audit', label: 'Audit log', icon: 'history' }, { route: 'fusion', label: 'Fusion', icon: 'zap' }, - { route: 'setup', label: 'Setup', icon: 'plug' }, ]; const NAV_DEV = [ { route: 'home', label: 'Overview', icon: 'home' }, { route: 'spend', label: 'Spend', icon: 'bar' }, { route: 'fusion', label: 'Fusion', icon: 'zap' }, - { route: 'setup', label: 'Setup', icon: 'plug' }, ]; // Monitor allowlist users (MONITOR_ACCESS_EMAILS) get the read-only Monitoring view. const NAV_MONITORING_ITEM = { route: 'monitoring', label: 'Monitoring', icon: 'activity' }; @@ -1228,7 +1225,7 @@ async function openAccountDrawer(a) { a.provider === 'cerebras' ? providerModelLimitsPanel(a, 'cerebras', 'gpt-oss-120b') : null, el('div', { class: 'field' }, el('label', { class: 'field-label' }, 'Notes'), - el('textarea', { id: 'acct_notes', placeholder: 'Internal notes…', on: { input: (e) => editNotes = e.target.value } }, editNotes), + el('textarea', { id: 'acct_notes', placeholder: 'Operator notes…', on: { input: (e) => editNotes = e.target.value } }, editNotes), ), el('div', { class: 'field-row' }, el('div', { class: 'field' }, @@ -1438,7 +1435,7 @@ function openAddAccountApiKey(providerHint) { el('div', { class: 'field-row' }, el('div', { class: 'field' }, el('label', { class: 'field-label' }, 'Label'), - el('input', { placeholder: 'e.g. nextbase.app', on: { input: (e) => label = e.target.value } }), + el('input', { placeholder: 'e.g. primary-account', on: { input: (e) => label = e.target.value } }), ), el('div', { class: 'field' }, el('label', { class: 'field-label' }, 'Owner email (optional)'), @@ -1790,7 +1787,7 @@ function openCreateUser() { const body = el('div', { class: 'field-group' }, el('div', { class: 'field' }, el('label', { class: 'field-label' }, 'Email'), - el('input', { type: 'email', placeholder: 'someone@nextbase.app', on: { input: (e) => email = e.target.value } }), + el('input', { type: 'email', placeholder: 'user@example.com', on: { input: (e) => email = e.target.value } }), ), el('div', { class: 'field' }, el('label', { class: 'field-label' }, 'Role'), @@ -3010,1564 +3007,6 @@ function wireGlobals() { }); } -/* ══════════════════════════════════════════════════════════════════ - ▓▓▓▓▓ SETUP PAGE ▓▓▓▓▓ - "Set up OCPlatform in 3 steps" hero + provider tabs. The selected - token (raw sp_* if known this session, else ) flows - into every snippet on the page. Visible to admins + developers. - ══════════════════════════════════════════════════════════════════ */ - -// In-memory + sessionStorage map of token id → raw sp_* secret. -// Populated only when the user creates a token in *this* tab; the -// server never re-returns the raw token after issuance, so any token -// created in another session stays as . -const RAW_TOKEN_STORE_KEY = 'sp.rawTokens.v1'; -const setupRawTokens = (() => { - try { - const raw = sessionStorage.getItem(RAW_TOKEN_STORE_KEY); - return raw ? JSON.parse(raw) : {}; - } catch { - return {}; - } -})(); -function rememberRawToken(id, raw) { - if (!id || !raw) return; - setupRawTokens[String(id)] = raw; - try { sessionStorage.setItem(RAW_TOKEN_STORE_KEY, JSON.stringify(setupRawTokens)); } catch {} -} -function rawTokenFor(id) { - if (id == null) return null; - return setupRawTokens[String(id)] || null; -} -let setupPreferredTokenId = null; - -const SETUP_TABS = [ - { id: 'anthropic', label: 'Anthropic' }, - { id: 'codex', label: 'Codex' }, - { id: 'images', label: 'Images' }, - { id: 'groq', label: 'Groq' }, - { id: 'cerebras', label: 'Cerebras' }, - { id: 'kimi', label: 'Kimi' }, - { id: 'glm', label: 'GLM' }, - { id: 'gemini', label: 'Gemini' }, - { id: 'openrouter', label: 'OpenRouter' }, - { id: 'xai', label: 'xAI' }, - { id: 'runpod', label: 'Runpod' }, - { id: 'deepgram', label: 'Deepgram' }, - { id: 'fusion', label: 'Fusion' }, - - { id: 'manual', label: 'Manual install' }, - { id: 'troubleshoot', label: 'Troubleshooting' }, -]; - -function currentSetupTab() { - const m = /(?:^|&)setup=([a-z]+)/.exec(location.hash || ''); - const id = m && m[1]; - return SETUP_TABS.find(t => t.id === id)?.id || 'anthropic'; -} -function setSetupTab(id) { - const hash = (location.hash || '').replace(/^#/, ''); - const parts = hash.split('&').filter(p => p && !p.startsWith('setup=')); - parts.push(`setup=${id}`); - location.hash = parts.join('&'); -} - -// Client axis (OCPlatform vs Hermes). The provider list is identical; only the -// wiring differs. Persisted in the hash like the tab state. -const SETUP_CLIENTS = [ - { id: 'openclaw', label: 'OCPlatform' }, - { id: 'hermes', label: 'Hermes' }, -]; -function currentSetupClient() { - const m = /(?:^|&)client=([a-z]+)/.exec(location.hash || ''); - const id = m && m[1]; - return SETUP_CLIENTS.find(c => c.id === id)?.id || 'openclaw'; -} -function setSetupClient(id) { - const hash = (location.hash || '').replace(/^#/, ''); - const parts = hash.split('&').filter(p => p && !p.startsWith('client=')); - parts.push(`client=${id}`); - location.hash = parts.join('&'); -} - -// Hermes custom_providers descriptors — mirror the live config.yaml shape. -// suffix appended to baseUrl; apiMode is the Hermes wire-protocol selector. -const HERMES_PROVIDERS = { - anthropic: { name: 'nextbase-anthropic', suffix: '', apiMode: 'anthropic_messages', model: 'claude-opus-4-6', models: ['claude-opus-4-6', 'claude-sonnet-4-5-20250929', 'claude-haiku-4-5'] }, - codex: { name: 'nextbase-codex', suffix: '/v1', apiMode: 'codex_responses', model: 'gpt-5.5', models: ['gpt-5.5', 'gpt-5.4', 'gpt-5.3-codex'] }, - xai: { name: 'nextbase-xai', suffix: '/v1/xai', apiMode: 'codex_responses', model: 'grok-4.3', models: ['grok-4.3', 'grok-4', 'grok-4-fast'] }, - groq: { name: 'nextbase-groq', suffix: '/v1/groq', apiMode: 'chat_completions', model: 'openai/gpt-oss-120b', models: ['openai/gpt-oss-120b', 'llama-3.1-8b-instant'] }, - cerebras: { name: 'nextbase-cerebras', suffix: '/v1/cerebras', apiMode: 'chat_completions', model: 'qwen-3-235b-a22b-instruct-2507', models: ['qwen-3-235b-a22b-instruct-2507', 'gpt-oss-120b', 'zai-glm-4.7'] }, - kimi: { name: 'nextbase-kimi', suffix: '/v1/kimi', apiMode: 'chat_completions', model: 'k3', models: ['k3', 'kimi-k2.7-code', 'kimi-k2.6', 'kimi-for-coding'] }, - glm: { name: 'nextbase-glm', suffix: '/v1/glm', apiMode: 'anthropic_messages', model: 'glm-5.2', models: ['glm-5.2', 'glm-5.1', 'glm-4.7', 'glm-4.6'] }, - openrouter: { name: 'nextbase-openrouter', suffix: '/v1/openrouter', apiMode: 'chat_completions', model: 'tencent/hy3:free', models: ['tencent/hy3:free'] }, -}; -function hermesProviderYaml(p, baseUrl) { - const modelsBlock = p.models.map(m => ` ${m}: {}`).join('\n'); - return `custom_providers:\n- name: ${p.name}\n base_url: ${baseUrl}${p.suffix}\n key_env: NEXTBASE_MODEL_GATEWAY_API_KEY\n api_mode: ${p.apiMode}\n model: ${p.model}\n models:\n${modelsBlock}\n discover_models: false`; -} -function hermesSetupBlock({ tabId, baseUrl, token, agentLine, verifyCurl, verifyNote }) { - const p = HERMES_PROVIDERS[tabId]; - if (!p) { - return el('div', { class: 'stack' }, - setupAgentCard(agentLine), - el('p', { class: 'fs-13 text-muted', style: { lineHeight: '1.55' } }, - 'This provider does not have a dedicated Hermes template yet. See the Hermes skill for the general custom_providers pattern.'), - el('div', { class: 'setup-block-label mt-2' }, 'Hermes setup skill'), - setupCodeBlock(`Read ${baseUrl}/skills/hermes/SKILL.md and set me up. My sp_* token is ${token}.`), - ); - } - const envLine = `echo 'NEXTBASE_MODEL_GATEWAY_API_KEY=${token}' >> ~/.hermes/.env`; - return el('div', { class: 'stack' }, - setupAgentCard(agentLine), - el('p', { class: 'fs-13 text-muted', style: { lineHeight: '1.55' } }, - 'Hermes uses ~/.hermes/config.yaml (custom_providers) and reads the token from an env var — not inline. The Anthropic billing path is detected automatically.'), - el('div', { class: 'setup-block-label' }, '1 · Token — add to ', el('code', { class: 'mono' }, '~/.hermes/.env')), - setupCodeBlock(envLine), - el('div', { class: 'setup-block-label mt-3' }, '2 · Provider — merge into ', el('code', { class: 'mono' }, '~/.hermes/config.yaml')), - setupCodeBlock(hermesProviderYaml(p, baseUrl)), - el('div', { class: 'setup-block-label mt-3' }, '3 · Restart Hermes, then verify'), - verifyNote ? el('p', { class: 'fs-12 text-muted', style: { lineHeight: '1.5' } }, verifyNote) : null, - verifyCurl ? setupCodeBlock(verifyCurl) : null, - ); -} - -function renderSetup() { - const root = el('div', { class: 'stack stack--lg setup-page' }); - const baseUrl = `${location.protocol}//${location.host}`; - const tokens = (state.data.tokens || []).filter(t => t.enabled !== false && t.enabled !== 0); - - // Selected token id is module-local to this render call but survives - // user actions (dropdown change, token creation) via paint(). - let activeTokenId = (setupPreferredTokenId != null && tokens.some(t => t.id === setupPreferredTokenId)) - ? setupPreferredTokenId - : (tokens[0]?.id ?? null); - setupPreferredTokenId = activeTokenId; - - // Track whether the raw token is currently revealed in the hero card. - let revealed = false; - // Latest token-check result (null = not run yet). - let tokenCheckResult = null; - - function effectiveToken() { - return activeTokenId != null ? rawTokenFor(activeTokenId) : null; - } - function tokenForSnippet() { - return effectiveToken() || ''; - } - - // ─── Hero card ──────────────────────────────────────────────── - const heroMount = el('section', { class: 'setup-hero card', 'aria-label': 'Set up OpenClaw in 3 steps' }); - const tabsMount = el('section', { class: 'setup-tabs-wrap' }); - root.appendChild(heroMount); - root.appendChild(tabsMount); - - function tokenSelector() { - if (!tokens.length) { - return el('div', { class: 'flex gap-2 items-center', style: { flexWrap: 'wrap' } }, - el('span', { class: 'fs-12 text-subtle' }, 'No active tokens yet.'), - el('button', { class: 'btn btn--primary btn--sm', on: { click: createTokenInline } }, - icon('plus', 14), 'Create token'), - ); - } - const sel = el('select', { - class: 'input setup-token-select', - 'aria-label': 'Active token', - on: { change: (e) => { activeTokenId = Number(e.target.value); setupPreferredTokenId = activeTokenId; revealed = false; tokenCheckResult = null; paint(); } }, - }, - ...tokens.map(t => { - const has = rawTokenFor(t.id) ? ' ✓' : ''; - return el('option', { value: t.id, ...(t.id === activeTokenId ? { selected: 'selected' } : {}) }, - `${t.label || 'token'} · ${t.token_prefix || ''}…${has}`); - }) - ); - return el('div', { class: 'flex gap-2 items-center setup-token-row', style: { flexWrap: 'wrap' } }, - sel, - el('button', { class: 'btn btn--ghost btn--sm', on: { click: createTokenInline } }, - icon('plus', 14), 'Create new token'), - ); - } - - async function createTokenInline() { - if (isAdminUser()) { - // Reuse the admin issue-token drawer for the currently selected user - // (the one who owns the active token) or for the admin themselves. - const meEmail = state.user?.email; - let targetUser = null; - if (activeTokenId != null) { - const t = tokens.find(x => x.id === activeTokenId); - if (t && Array.isArray(state.data.users)) { - targetUser = state.data.users.find(u => u.id === t.user_id || u.email === t.user_email) || null; - } - } - if (!targetUser && Array.isArray(state.data.users)) { - targetUser = state.data.users.find(u => u.email === meEmail) || state.data.users[0] || null; - } - if (!targetUser) { - toast('No user available to issue a token for. Create a user first.', 'error'); - return; - } - // Patch openIssueToken's submit path to also remember the raw token. - // Simpler: wrap fetch via api() — but openIssueToken does its own fetch. - // Trick: monkey-patch toast-after-refresh by listening to refresh(). - // Cleanest path: just open the drawer; after the user copies, the - // dropdown won't auto-substitute. Acceptable trade-off but spec wants - // substitution, so we call the admin endpoint directly with a small - // inline helper instead. - openSetupIssueToken(targetUser); - } else { - // Quick self-issue. - try { - const r = await api('/api/me/tokens', { - method: 'POST', - body: JSON.stringify({ label: 'setup-quick' }), - }); - const created = r.token; - if (r.id != null && created) rememberRawToken(r.id, created); - if (r.id != null) { activeTokenId = r.id; setupPreferredTokenId = r.id; } - await refresh(); - toast('Token created — autofilled into the snippets below'); - // refresh() already triggered render(), but the new activeTokenId - // we just set would be lost in the fresh renderSetup call. So we - // also rely on default = tokens[0]. Acceptable: new tokens go to - // the top of the list in most loaders. If not, the user can pick. - } catch (e) { - toast(e.message || 'Failed to create token', 'error'); - } - } - } - - // Lightweight wrapper around openIssueToken that captures the raw token - // returned by the admin API and remembers it in sessionStorage so the - // setup snippets get auto-substituted. - function openSetupIssueToken(u) { - let label = 'setup-quick'; - let capUsd = ''; - let capTokens = ''; - let createdToken = null; - let createdId = null; - - const formBody = el('div', { class: 'field-group' }, - el('div', { class: 'field' }, - el('label', { class: 'field-label' }, 'Label'), - el('input', { value: label, on: { input: (e) => label = e.target.value } }), - ), - el('div', { class: 'field-row' }, - el('div', { class: 'field' }, - el('label', { class: 'field-label' }, 'Cap · daily $ (optional)'), - el('input', { type: 'number', placeholder: 'unlimited', on: { input: (e) => capUsd = e.target.value } }), - ), - el('div', { class: 'field' }, - el('label', { class: 'field-label' }, 'Cap · daily tokens (optional)'), - el('input', { type: 'number', placeholder: 'unlimited', on: { input: (e) => capTokens = e.target.value } }), - ), - ), - ); - const revealBody = el('div', { class: 'stack hidden' }); - const body = el('div', {}, formBody, revealBody); - const primary = el('button', { class: 'btn btn--primary', on: { click: submit } }, icon('plus', 14), 'Issue token'); - const footer = el('div', { class: 'flex gap-2', style: { width: '100%' } }, - el('div', { class: 'spacer' }), - el('button', { class: 'btn btn--ghost', on: { click: () => d.close() } }, 'Done'), - primary, - ); - - async function submit() { - try { - const r = await api(`/admin/users/${u.id}/tokens`, { - method: 'POST', - body: JSON.stringify({ - label, - capUsdDaily: capUsd === '' ? null : Number(capUsd), - capTokensDaily: capTokens === '' ? null : Number(capTokens), - }), - }); - createdToken = r.token; - createdId = r.id ?? r.tokenId ?? null; - if (createdId != null && createdToken) { - rememberRawToken(createdId, createdToken); - setupPreferredTokenId = createdId; - } - formBody.classList.add('hidden'); - revealBody.classList.remove('hidden'); - revealBody.replaceChildren( - el('div', { class: 'alert alert--success' }, icon('check', 14), - el('div', {}, el('div', { class: 'fw-600' }, 'Token created'), - el('div', { class: 'fs-12 mt-1' }, 'Copy it now — you won\'t see it again. We also kept it in this browser tab\'s memory so the Setup snippets fill in automatically.'))), - el('div', { class: 'code mt-2' }, createdToken, - el('button', { class: 'btn btn--ghost btn--sm copy-btn', on: { click: () => copyToClipboard(createdToken) } }, - icon('copy', 14), 'Copy')), - ); - primary.classList.add('hidden'); - if (createdId != null) { activeTokenId = createdId; setupPreferredTokenId = createdId; } - await refresh(); - } catch (e) { toast(e.message, 'error'); } - } - const d = drawer({ title: 'Issue token', subtitle: `for ${u.email}`, body, footer }); - } - - // ─── Step 3: token check ────────────────────────────────────── - async function runTokenCheck() { - const tok = effectiveToken(); - if (!tok) { - tokenCheckResult = { kind: 'amber', message: 'Paste or create a token first — the dropdown only knows the token prefix, not the secret.' }; - paint(); - return; - } - tokenCheckResult = { kind: 'pending' }; - paint(); - try { - const r = await fetch(`${baseUrl}/v1/token/check`, { - method: 'GET', - headers: { Authorization: `Bearer ${tok}` }, - }); - const txt = await r.text(); - let body = null; - try { body = JSON.parse(txt); } catch { body = { raw: txt }; } - if (r.ok && body?.ok) { - tokenCheckResult = { kind: 'ok', body }; - } else { - tokenCheckResult = { kind: 'fail', status: r.status, body }; - } - } catch (e) { - tokenCheckResult = { kind: 'fail', network: true, message: e?.message || 'Network error' }; - } - paint(); - } - - function tokenCheckPanel() { - if (!tokenCheckResult) { - return el('div', { class: 'notice notice--info' }, - 'Click ', el('strong', {}, 'Run token check'), ' to verify the selected token against ', el('code', { class: 'mono' }, '/v1/token/check'), '. No model request, no credits spent.'); - } - if (tokenCheckResult.kind === 'pending') { - return el('div', { class: 'notice notice--info' }, 'Checking token…'); - } - if (tokenCheckResult.kind === 'amber') { - return el('div', { class: 'alert alert--warn' }, icon('alert', 14), el('div', {}, tokenCheckResult.message)); - } - if (tokenCheckResult.kind === 'ok') { - const b = tokenCheckResult.body || {}; - const u = b.user || {}; - const t = b.token || {}; - return el('div', { class: 'alert alert--success' }, icon('check', 14), - el('div', {}, - el('div', { class: 'fw-600' }, '✅ Token works'), - el('div', { class: 'fs-12 mt-1' }, - (u.email || 'user'), ' · ', (u.role || 'developer'), - ' · token ', el('code', { class: 'mono' }, t.label || t.prefix || `#${t.id ?? ''}`)), - )); - } - // fail - const r = tokenCheckResult; - const msg = r.network - ? `Network error: ${r.message}` - : `HTTP ${r.status} — ${r.body?.error || r.body?.detail || r.body?.raw || 'token rejected'}`; - return el('div', { class: 'alert alert--danger' }, icon('alert', 14), - el('div', {}, - el('div', { class: 'fw-600' }, '❌ Token rejected'), - el('div', { class: 'fs-12 mt-1' }, msg, ' — ', - el('a', { href: 'javascript:void(0)', on: { click: (e) => { e.preventDefault(); setSetupTab('troubleshoot'); } } }, 'See troubleshooting')), - )); - } - - // ─── Build hero body ───────────────────────────────────────── - function buildHero() { - const tok = effectiveToken(); - const masked = tok ? tok.slice(0, 12) + '…' + tok.slice(-4) : null; - - const step1 = el('div', { class: 'setup-step' }, - el('div', { class: 'setup-step__num' }, '1'), - el('div', { class: 'setup-step__body' }, - el('div', { class: 'setup-step__title' }, 'Pick (or create) a token'), - el('div', { class: 'setup-step__sub fs-12 text-muted' }, - 'Tokens authenticate every request through this gateway. The raw ', - el('code', { class: 'mono' }, 'sp_*'), ' value is only shown once at creation — we keep it in this browser tab\'s memory to autofill the snippets below.'), - el('div', { class: 'mt-2' }, tokenSelector()), - tok - ? el('div', { class: 'setup-token-preview mt-2' }, - el('span', { class: 'fs-12 text-subtle' }, 'Token: '), - el('code', { class: 'mono setup-token-value' }, revealed ? tok : masked), - el('button', { - class: 'btn btn--ghost btn--sm', - on: { click: () => { revealed = !revealed; paint(); } }, - }, revealed ? 'Hide' : 'Reveal'), - el('button', { - class: 'btn btn--ghost btn--sm', - on: { click: () => copyToClipboard(tok) }, - }, icon('copy', 12), 'Copy'), - ) - : el('div', { class: 'notice notice--warn mt-2' }, - 'Active token\'s raw value isn\'t in this browser tab. The snippets below will keep ', - el('code', { class: 'mono' }, ''), ' as a placeholder — replace it with the value you saved when the token was created, or create a new token here.'), - tok ? el('div', { class: 'fs-12 text-subtle mt-2' }, - 'Heads up: once you leave this page (close the tab or sign out), the full token won\'t be shown again.') : null, - ), - ); - - const agentPrompt = buildAgentSetupPrompt(baseUrl, null, tokenForSnippet()); - const tellAgent = el('div', { class: 'setup-step setup-step--agent' }, - el('div', { class: 'setup-step__num' }, icon('plug', 16)), - el('div', { class: 'setup-step__body' }, - el('div', { class: 'setup-step__title' }, 'Tell an agent to set itself up'), - el('div', { class: 'setup-step__sub fs-12 text-muted' }, - 'Paste this into OCPlatform, Claude Code, Codex CLI, Kimi CLI, or any LLM-driven agent. It will read the skill and wire the gateway.'), - setupCodeBlock(agentPrompt), - ), - ); - - const oneShot = buildOneShotScript(baseUrl, tokenForSnippet()); - const scriptToggle = el('details', { class: 'setup-script' }, - el('summary', {}, 'Show full script'), - el('div', { class: 'code mt-2' }, oneShot, - el('button', { class: 'btn btn--ghost btn--sm copy-btn', on: { click: () => copyToClipboard(oneShot) } }, - icon('copy', 14), 'Copy')), - ); - const step2 = el('div', { class: 'setup-step' }, - el('div', { class: 'setup-step__num' }, '2'), - el('div', { class: 'setup-step__body' }, - el('div', { class: 'setup-step__title' }, 'Run the one-shot setup on the OCPlatform machine'), - el('div', { class: 'setup-step__sub fs-12 text-muted' }, - 'Patches ', el('code', { class: 'mono' }, '~/.openclaw/openclaw.json'), ', ', - el('code', { class: 'mono' }, 'models.json'), ', and ', - el('code', { class: 'mono' }, 'auth-profiles.json'), ' (with backups), then restarts ', - el('code', { class: 'mono' }, 'openclaw-gateway.service'), ' if present.'), - el('div', { class: 'flex gap-2 mt-2', style: { flexWrap: 'wrap' } }, - el('button', { class: 'btn btn--primary btn--sm', on: { click: () => copyToClipboard(oneShot) } }, - icon('copy', 14), 'Copy one-shot setup'), - ), - scriptToggle, - ), - ); - - const step3 = el('div', { class: 'setup-step' }, - el('div', { class: 'setup-step__num' }, '3'), - el('div', { class: 'setup-step__body' }, - el('div', { class: 'setup-step__title' }, 'Verify the token reaches the gateway'), - el('div', { class: 'setup-step__sub fs-12 text-muted' }, - 'Calls ', el('code', { class: 'mono' }, 'GET /v1/token/check'), ' with the selected token. No model request, no spend.'), - el('div', { class: 'flex gap-2 mt-2', style: { flexWrap: 'wrap' } }, - el('button', { class: 'btn btn--primary btn--sm', on: { click: runTokenCheck } }, - icon('check', 14), 'Run token check'), - ), - el('div', { class: 'mt-2' }, tokenCheckPanel()), - ), - ); - - return el('div', { class: 'setup-hero__inner' }, - el('div', { class: 'setup-hero__head' }, - el('div', {}, - el('div', { class: 'setup-hero__eyebrow' }, 'Get started'), - el('h2', { class: 'setup-hero__title' }, `Set up ${currentSetupClient() === 'hermes' ? 'Hermes' : 'OCPlatform'} in 3 steps`), - el('div', { class: 'setup-hero__sub fs-12 text-muted' }, - 'Gateway base URL · ', el('code', { class: 'mono' }, baseUrl)), - ), - el('div', { class: 'flex gap-2 items-center' }, - el('button', { class: 'btn btn--ghost btn--sm', on: { click: () => copyToClipboard(baseUrl) } }, - icon('copy', 14), 'Copy URL'), - ), - ), - el('ol', { class: 'setup-steps-v2' }, step1, tellAgent, step2, step3), - ); - } - - // ─── Tabs ───────────────────────────────────────────────────── - function buildClientToggle() { - const active = currentSetupClient(); - return el('div', { class: 'setup-client-toggle flex gap-2 items-center', style: { marginBottom: '8px' } }, - el('span', { class: 'fs-12 text-muted' }, 'Client:'), - el('div', { class: 'tabs setup-client-tabs', role: 'tablist', 'aria-label': 'Agent client' }, - ...SETUP_CLIENTS.map(c => - el('button', { - class: 'tab' + (c.id === active ? ' active' : ''), - role: 'tab', - 'aria-selected': c.id === active ? 'true' : 'false', - on: { click: () => { setSetupClient(c.id); paint(); } }, - }, c.label) - ), - ), - ); - } - - function buildTabs() { - const active = currentSetupTab(); - const strip = el('div', { class: 'tabs setup-tabs', role: 'tablist', 'aria-label': 'Provider configuration' }, - ...SETUP_TABS.map(t => - el('button', { - class: 'tab' + (t.id === active ? ' active' : ''), - role: 'tab', - 'aria-selected': t.id === active ? 'true' : 'false', - 'aria-controls': `setup-panel-${t.id}`, - id: `setup-tab-${t.id}`, - on: { click: () => { setSetupTab(t.id); paint(); } }, - }, t.label) - ), - ); - - const panel = el('div', { - class: 'setup-tab-panel', - role: 'tabpanel', - id: `setup-panel-${active}`, - 'aria-labelledby': `setup-tab-${active}`, - }); - - const client = currentSetupClient(); - const ctx = { baseUrl, token: tokenForSnippet(), tokenIsReal: !!effectiveToken(), client, tabId: active }; - if (client === 'hermes' && HERMES_PROVIDERS[active]) { - // Hermes: render the YAML custom_providers wiring for this provider. - const verifyCurl = active === 'anthropic' - ? `curl -sS -i \\ - -H "Authorization: Bearer ${ctx.token}" \\ - -H "Content-Type: application/json" \\ - -H "anthropic-version: 2023-06-01" \\ - -d '{"model":"claude-haiku-4-5","max_tokens":16,"messages":[{"role":"user","content":"pong"}]}' \\ - ${baseUrl}/v1/messages` - : `curl -sS -i \\ - -H "Authorization: Bearer ${ctx.token}" \\ - ${baseUrl}/v1/token/check`; - panel.appendChild(hermesSetupBlock({ - tabId: active, baseUrl, token: ctx.token, - agentLine: `Read ${baseUrl}/skills/hermes/SKILL.md and set me up for ${HERMES_PROVIDERS[active].name}. My sp_* token is ${ctx.token}.`, - verifyCurl, - verifyNote: active === 'anthropic' - ? 'Should return HTTP/2 200 with the assistant text and an x-gateway-account header.' - : 'Should return HTTP/2 200 with {"ok":true} — confirms the token before you wire the provider.', - })); - } else { - const builder = TAB_BUILDERS[active] || TAB_BUILDERS.anthropic; - panel.appendChild(builder(ctx)); - } - - return el('div', {}, buildClientToggle(), strip, panel); - } - - function paint() { - heroMount.replaceChildren(); - heroMount.appendChild(buildHero()); - tabsMount.replaceChildren(); - tabsMount.appendChild(buildTabs()); - } - - paint(); - return root; -} - -/* ─── Snippet helpers reused by every tab ──────────────────── */ -function setupCodeBlock(content) { - return el('div', { class: 'code mt-2' }, content, - el('button', { class: 'btn btn--ghost btn--sm copy-btn', on: { click: () => copyToClipboard(content) } }, - icon('copy', 14), 'Copy')); -} -function buildAgentSetupPrompt(_baseUrl, slug, token) { - const skillPath = slug ? `/skills/${slug}/SKILL.md` : '/skills/SKILL.md'; - return `Read http://localhost:8080${skillPath} and set me up. My sp_* token is ${token}.`; -} -function setupAgentCard(agentLine) { - if (!agentLine) return null; - return el('div', { class: 'card setup-agent-card' }, - el('div', { class: 'setup-block-label' }, 'Tell an agent'), - el('p', { class: 'fs-12 text-muted', style: { lineHeight: '1.5' } }, - 'Paste this into another LLM-driven agent so it can read the provider skill and wire itself.'), - setupCodeBlock(agentLine), - ); -} -function setupProviderBlock({ desc, providerJson, authProfile, verifyCurl, verifyNote, extra, agentLine }) { - return el('div', { class: 'stack' }, - setupAgentCard(agentLine), - desc ? el('p', { class: 'fs-13 text-muted', style: { lineHeight: '1.55' } }, desc) : null, - el('div', { class: 'setup-block-label' }, 'Provider block — merge into ', el('code', { class: 'mono' }, 'openclaw.json'), ' and ', el('code', { class: 'mono' }, 'models.json')), - setupCodeBlock(providerJson), - authProfile ? el('div', { class: 'setup-block-label mt-3' }, 'Auth profile — merge into ', el('code', { class: 'mono' }, 'auth-profiles.json')) : null, - authProfile ? setupCodeBlock(authProfile) : null, - extra || null, - verifyCurl ? el('div', { class: 'setup-block-label mt-3' }, 'Verify') : null, - verifyNote ? el('p', { class: 'fs-12 text-muted', style: { lineHeight: '1.5' } }, verifyNote) : null, - verifyCurl ? setupCodeBlock(verifyCurl) : null, - ); -} - -/* ─── Tab content builders ─────────────────────────────────── */ -const TAB_BUILDERS = { - anthropic({ baseUrl, token }) { - const providerJson = `{ - "models": { - "providers": { - "anthropic": { - "baseUrl": "${baseUrl}", - "authHeader": true, - "models": [] - } - } - } -}`; - const authProfile = `{ - "profiles": { - "anthropic:manual": { - "type": "token", - "provider": "anthropic", - "token": "${token}" - } - } -}`; - const verifyCurl = `curl -sS -i \\ - -H "Authorization: Bearer ${token}" \\ - -H "Content-Type: application/json" \\ - -H "anthropic-version: 2023-06-01" \\ - -d '{"model":"claude-sonnet-4-5-20250929","max_tokens":16,"messages":[{"role":"user","content":"pong"}]}' \\ - ${baseUrl}/v1/messages`; - return setupProviderBlock({ - desc: 'Routes /v1/messages and /v1/messages/count_tokens through this gateway. authHeader: true forces Bearer instead of x-api-key — that\'s what the gateway expects.', - providerJson, authProfile, verifyCurl, - agentLine: buildAgentSetupPrompt(baseUrl, 'anthropic', token), - verifyNote: 'Should return HTTP/2 200 with the assistant text and an x-gateway-account header.', - }); - }, - - glm({ baseUrl, token }) { - const providerJson = `{ - "models": { - "providers": { - "glm": { - "baseUrl": "${baseUrl}/v1/glm", - "api": "anthropic-messages", - "authHeader": true, - "apiKey": "${token}", - "models": [ - { "id": "glm-5.2", "name": "GLM 5.2" } - ] - } - } - } -}`; - const authProfile = `{ - "profiles": { - "glm:manual": { - "type": "token", - "provider": "glm", - "token": "${token}" - } - } -}`; - const verifyCurl = `curl -sS -i \\ - -H "x-api-key: ${token}" \\ - -H "Content-Type: application/json" \\ - -H "anthropic-version: 2023-06-01" \\ - -d '{"model":"glm-5.2","max_tokens":16,"messages":[{"role":"user","content":"hi"}]}' \\ - ${baseUrl}/v1/glm/v1/messages`; - return setupProviderBlock({ - desc: 'GLM is an Anthropic-format (Anthropic Messages) provider — a transparent passthrough to the z.ai GLM Coding Plan. It behaves exactly like the Anthropic tab: Anthropic SDKs append /v1/messages to the baseUrl ' + baseUrl + '/v1/glm, so the canonical client path is ' + baseUrl + '/v1/glm/v1/messages (the short alias ' + baseUrl + '/v1/glm/messages also works). authHeader: true forces Bearer; the gateway also accepts x-api-key. Default model glm-5.2 has native 1M context / 128K max output.', - providerJson, authProfile, verifyCurl, - agentLine: buildAgentSetupPrompt(baseUrl, 'glm', token), - verifyNote: 'Expect HTTP/2 200 with x-gateway-provider: glm. If you get model_not_allowed_for_user, enable GLM for that user first. Valid models: glm-5.2, glm-5.1, glm-5, glm-5-turbo, glm-4.7, glm-4.6, glm-4.5 (there is no -1m / [1m] id variant — those 400).', - }); - }, - - codex({ baseUrl, token }) { - const providerJson = `{ - "models": { - "providers": { - "openai-codex": { - "baseUrl": "${baseUrl}/v1", - "api": "openai-responses", - "apiKey": "${token}", - "models": [ - { - "id": "gpt-5.5", - "name": "GPT-5.5", - "reasoning": true, - "input": ["text", "image"], - "contextWindow": 400000, - "maxTokens": 128000 - } - ] - } - } - } -}`; - const authProfile = `{ - "profiles": { - "openai-codex:nextbase-gateway": { - "type": "api_key", - "provider": "openai-codex", - "key": "${token}" - } - } -}`; - const verifyCurl = `curl -sS -N \\ - -H "Authorization: Bearer ${token}" \\ - -H "Content-Type: application/json" \\ - -d '{"model":"gpt-5.5","stream":true,"input":[{"role":"user","content":[{"type":"input_text","text":"pong"}]}]}' \\ - ${baseUrl}/v1/responses`; - return setupProviderBlock({ - desc: 'Use the openai-codex/gpt-* PI route for Super Proxy. Do NOT use openai/gpt-* + agentRuntime.id: "codex" for gateway-routed traffic — native Codex talks to ChatGPT directly and bypasses this gateway. The /v1 suffix on baseUrl is required and api must be "openai-responses".', - providerJson, authProfile, verifyCurl, - agentLine: buildAgentSetupPrompt(baseUrl, 'openai-codex', token), - verifyNote: 'Returns an SSE stream of Responses API events. The first event should arrive within a second.', - }); - }, - - images({ baseUrl, token }) { - const providerJson = `{ - "models": { - "providers": { - "openai": { - "baseUrl": "${baseUrl}/v1", - "apiKey": "${token}", - "models": [] - } - } - } -}`; - const authProfile = `{ - "profiles": { - "openai:nextbase-gateway": { - "type": "api_key", - "provider": "openai", - "key": "${token}" - } - } -}`; - const verifyCurl = `curl -sS -i \\ - -H "Authorization: Bearer ${token}" \\ - -H "Content-Type: application/json" \\ - -d '{"model":"gpt-image-2","prompt":"a small duck swimming in water","size":"1024x1024","n":1}' \\ - ${baseUrl}/v1/images/generations`; - return setupProviderBlock({ - desc: 'gpt-image-2 routes through the regular openai provider (NOT openai-codex). The /v1 suffix is required so calls land on /v1/images/generations and /v1/images/edits. If the gateway has no real OpenAI key, Super Proxy falls back to Codex/ChatGPT OAuth via the Responses API image_generation tool — the response shape stays { data: [{ b64_json }] }.', - providerJson, authProfile, verifyCurl, - agentLine: buildAgentSetupPrompt(baseUrl, 'openai-images', token), - verifyNote: 'Look for x-gateway-image-mode in the response headers. Success = HTTP/2 200 with base64 image data.', - }); - }, - - groq({ baseUrl, token }) { - const providerJson = `{ - "models": { - "providers": { - "groq": { - "baseUrl": "${baseUrl}/v1/groq", - "apiKey": "${token}", - "models": [ - { "id": "openai/gpt-oss-120b", "name": "Groq GPT OSS 120B" } - ] - } - } - } -}`; - const authProfile = `{ - "profiles": { - "groq:nextbase-gateway": { - "type": "api_key", - "provider": "groq", - "key": "${token}" - } - } -}`; - const verifyCurl = `curl -sS -i \\ - -H "Authorization: Bearer ${token}" \\ - -H "Content-Type: application/json" \\ - -d '{"model":"openai/gpt-oss-120b","messages":[{"role":"user","content":"pong"}]}' \\ - ${baseUrl}/v1/groq/chat/completions`; - return setupProviderBlock({ - desc: 'OpenAI-compatible Groq chat and embeddings under /v1/groq.', - providerJson, authProfile, verifyCurl, - agentLine: buildAgentSetupPrompt(baseUrl, 'groq', token), - verifyNote: 'Expect HTTP/2 200 with a choices array.', - }); - }, - - cerebras({ baseUrl, token }) { - const providerJson = `{ - "models": { - "providers": { - "cerebras": { - "baseUrl": "${baseUrl}/v1/cerebras", - "apiKey": "${token}", - "models": [ - { "id": "gpt-oss-120b", "name": "Cerebras GPT OSS 120B" }, - { "id": "zai-glm-4.7", "name": "Cerebras Z.ai GLM 4.7" } - ] - } - } - } -}`; - const authProfile = `{ - "profiles": { - "cerebras:nextbase-gateway": { - "type": "api_key", - "provider": "cerebras", - "key": "${token}" - } - } -}`; - const verifyCurl = `curl -sS -i \ - -H "Authorization: Bearer ${token}" \ - -H "Content-Type: application/json" \ - -d '{"model":"gpt-oss-120b","messages":[{"role":"user","content":"pong"}]}' \ - ${baseUrl}/v1/cerebras/chat/completions`; - return setupProviderBlock({ - desc: 'OCPlatform — Cerebras. OpenAI-compatible Cerebras chat and embeddings under /v1/cerebras.', - providerJson, authProfile, verifyCurl, - agentLine: buildAgentSetupPrompt(baseUrl, 'cerebras', token), - verifyNote: 'Expect HTTP/2 200 with a choices array.', - }); - }, - - kimi({ baseUrl, token }) { - const providerJson = `{ - "models": { - "providers": { - "kimi": { - "baseUrl": "${baseUrl}/v1/kimi", - "apiKey": "${token}", - "models": [ - { "id": "k3", "name": "Kimi K3" }, - { "id": "kimi-k2.7-code", "name": "Kimi K2.7 Code" }, - { "id": "kimi-k2.6", "name": "Kimi K2.6" }, - { "id": "kimi-for-coding", "name": "Kimi for Coding" } - ] - }, - "kimi-anthropic": { - "baseUrl": "${baseUrl}/v1/kimi", - "authHeader": true, - "models": [] - }, - "openrouter": { - "baseUrl": "${baseUrl}/v1/openrouter", - "apiKey": "${token}", - "models": [ - { "id": "tencent/hy3:free", "name": "Tencent HY3 Free" } - ] - }, - "deepgram": { - "baseUrl": "${baseUrl}/v1/deepgram", - "apiKey": "${token}", - "models": [ - { "id": "nova-3", "name": "Deepgram Nova-3" }, - { "id": "nova-2", "name": "Deepgram Nova-2" }, - { "id": "whisper", "name": "Deepgram Whisper Cloud" } - ] - } - } - } -}`; - const authProfile = `{ - "profiles": { - "kimi:nextbase-gateway": { - "type": "api_key", - "provider": "kimi", - "key": "${token}" - }, - "kimi-anthropic:nextbase-gateway": { - "type": "token", - "provider": "kimi-anthropic", - "token": "${token}" - } - } -}`; - const verifyCurl = `curl -sS -i \\ - -H "Authorization: Bearer ${token}" \\ - -H "Content-Type: application/json" \\ - -d '{"model":"k3","reasoning_effort":"max","messages":[{"role":"user","content":"pong"}]}' \\ - ${baseUrl}/v1/kimi/chat/completions`; - const anthropicCurl = `curl -sS -i \\ - -H "Authorization: Bearer ${token}" \\ - -H "Content-Type: application/json" \\ - -H "anthropic-version: 2023-06-01" \\ - -d '{"model":"k3","max_tokens":16,"messages":[{"role":"user","content":"pong"}]}' \\ - ${baseUrl}/v1/kimi/messages`; - return setupProviderBlock({ - desc: 'Two shapes under /v1/kimi: OpenAI-compatible /chat/completions (kimi provider) and Anthropic-compatible /messages (kimi-anthropic provider).', - providerJson, authProfile, verifyCurl, - agentLine: buildAgentSetupPrompt(baseUrl, 'kimi', token), - verifyNote: 'OpenAI-style chat completion. The Anthropic-style endpoint uses /v1/kimi/messages and the same Bearer token.', - extra: el('div', { class: 'mt-3' }, - el('div', { class: 'setup-block-label' }, 'Anthropic-shape verify'), - setupCodeBlock(anthropicCurl)), - }); - }, - - gemini({ baseUrl, token }) { - // Primary use: pooled free-tier embeddings for OpenClaw memorySearch (8 keys -> rotation + failover). - const memoryJson = `{ - "agents": { - "main": { - "memorySearch": { - "enabled": true, - "provider": "openai", - "model": "gemini-embedding-2", - "remote": { - "baseUrl": "${baseUrl}/v1/gemini", - "apiKey": "${token}" - } - } - } - } -}`; - const providerJson = `{ - "models": { - "providers": { - "gemini-nextbase": { - "baseUrl": "${baseUrl}/v1/gemini", - "apiKey": "${token}", - "models": [ - { "id": "gemini-3.1-flash-lite", "name": "Gemini 3.1 Flash Lite" } - ] - } - } - } -}`; - const embedCurl = `curl -sS -i \\ - -H "Authorization: Bearer ${token}" \\ - -H "Content-Type: application/json" \\ - -d '{"model":"gemini-embedding-2","input":"pong"}' \\ - ${baseUrl}/v1/gemini/embeddings`; - const chatCurl = `curl -sS -i \\ - -H "Authorization: Bearer ${token}" \\ - -H "Content-Type: application/json" \\ - -d '{"model":"gemini-3.1-flash-lite","messages":[{"role":"user","content":"pong"}]}' \\ - ${baseUrl}/v1/gemini/chat/completions`; - return setupProviderBlock({ - desc: 'Native Gemini free-tier pool (separate from OpenRouter\'s Gemini). Three OpenAI-compatible routes under /v1/gemini: /embeddings, /chat/completions, /tts. The main use is pooled embeddings for OCPlatform memorySearch across multiple free-tier keys (rotation + 429 failover). Distinct from the openrouter provider above.', - providerJson, authProfile: memoryJson, verifyCurl: embedCurl, - agentLine: buildAgentSetupPrompt(baseUrl, 'gemini', token), - verifyNote: 'Embeddings return an OpenAI-shape {data:[{embedding:[...3072 floats]}]}. The authProfile block above is the memorySearch config (not an auth profile) — wire it into agents.main.memorySearch.', - extra: el('div', { class: 'mt-3' }, - el('div', { class: 'setup-block-label' }, 'Chat verify (gemini-3.1-flash-lite)'), - setupCodeBlock(chatCurl)), - }); - }, - - - openrouter({ baseUrl, token }) { - const providerJson = `{ - "models": { - "providers": { - "openrouter": { - "baseUrl": "${baseUrl}/v1/openrouter", - "apiKey": "${token}", - "models": [ - { "id": "tencent/hy3:free", "name": "Tencent HY3 Free" } - ] - } - } - } -}`; - const authProfile = `{ - "profiles": { - "openrouter:nextbase-gateway": { - "type": "api_key", - "provider": "openrouter", - "key": "${token}" - } - } -}`; - const verifyCurl = `curl -sS -i \ - -H "Authorization: Bearer ${token}" \ - -H "Content-Type: application/json" \ - -d '{"model":"tencent/hy3:free","messages":[{"role":"user","content":"pong"}],"max_tokens":16}' \ - ${baseUrl}/v1/openrouter/chat/completions`; - return setupProviderBlock({ - desc: 'OpenRouter is exposed as a strict Gemini-only provider under /v1/openrouter. Non-admin users are disabled by default; enable OpenRouter per user in Model access when needed.', - providerJson, authProfile, verifyCurl, - agentLine: buildAgentSetupPrompt(baseUrl, 'openrouter', token), - verifyNote: 'Expect HTTP/2 200 with a choices array. If you get model_not_allowed_for_user, enable OpenRouter for that user first.', - }); - }, - - deepgram({ baseUrl, token }) { - const providerJson = `{ - "models": { - "providers": { - "deepgram": { - "baseUrl": "${baseUrl}/v1/deepgram", - "apiKey": "${token}", - "models": [ - { "id": "nova-3", "name": "Deepgram Nova-3" }, - { "id": "nova-2", "name": "Deepgram Nova-2" }, - { "id": "whisper", "name": "Deepgram Whisper Cloud" } - ] - } - } - } -}`; - const authProfile = `{ - "profiles": { - "deepgram:nextbase-gateway": { - "type": "api_key", - "provider": "deepgram", - "key": "${token}" - } - } -}`; - const verifyCurl = `curl -sS -i \ - -H "Authorization: Bearer ${token}" \ - -H "Content-Type: application/json" \ - -d '{"url":"https://dpgr.am/spacewalk.wav"}' \ - ${baseUrl}/v1/deepgram/listen?model=nova-3&smart_format=true`; - const rawAudioCurl = `curl -sS -i \ - -H "Authorization: Bearer ${token}" \ - -H "Content-Type: audio/wav" \ - --data-binary @your-audio.wav \ - ${baseUrl}/v1/deepgram/listen?model=nova-3&smart_format=true`; - return setupProviderBlock({ - desc: 'Deepgram transcription is available under /v1/deepgram/listen. It supports JSON URL payloads and raw audio uploads. Unknown Deepgram models are blocked before upstream.', - providerJson, authProfile, verifyCurl, - agentLine: buildAgentSetupPrompt(baseUrl, 'deepgram', token), - verifyNote: 'Expect HTTP/2 200 with Deepgram results.metadata.duration and results.channels[].alternatives[].transcript.', - extra: el('div', { class: 'mt-3' }, - el('div', { class: 'setup-block-label' }, 'Raw audio upload verify'), - setupCodeBlock(rawAudioCurl)), - }); - }, - - - xai({ baseUrl, token }) { - const providerJson = `{ - "models": { - "providers": { - "xai": { - "baseUrl": "${baseUrl}/v1/xai", - "api": "openai-responses", - "apiKey": "***", - "models": [ - { "id": "grok-4.3", "name": "Grok 4.3", "reasoning": true, "input": ["text", "image"] } - ] - }, - "runpod": { - "baseUrl": "${baseUrl}/v1/runpod", - "apiKey": "${token}", - "models": [ - { "id": "qwen36-27b", "name": "Qwen3.6 27B" }, - { "id": "qwen36-27b-fast", "name": "Qwen3.6 27B Fast" } - ] - } - } - } -}`; - const authProfile = `{ - "profiles": { - "xai:nextbase-gateway": { - "type": "api_key", - "provider": "xai", - "key": "${token}" - } - } -}`; - const verifyCurl = `curl -sS -i \ - -H "Authorization: Bearer ${token}" \ - -H "Content-Type: application/json" \ - -d '{"model":"grok-4.3","input":"pong"}' \ - ${baseUrl}/v1/xai/responses`; - return setupProviderBlock({ - desc: 'xAI Grok is exposed through /v1/xai/responses, including Agent Tools such as web_search via the Responses tools array. Grok Voice uses /v1/xai/realtime/client_secrets, /v1/xai/tts, and /v1/xai/stt. Grok Imagine image/video generation lives under /v1/xai/images/* and /v1/xai/videos/*. Non-admin users are disabled by default; enable xAI per user in Model access when needed.', - providerJson, authProfile, verifyCurl, - agentLine: buildAgentSetupPrompt(baseUrl, 'xai', token), - verifyNote: 'Expect HTTP/2 200 with x-gateway-provider: xai. If you get model_not_allowed_for_user, enable xAI for that user first.', - }); - }, - - runpod({ baseUrl, token }) { - const providerJson = `{ - "models": { - "providers": { - "runpod": { - "baseUrl": "${baseUrl}/v1/runpod", - "apiKey": "${token}", - "models": [ - { "id": "qwen36-27b", "name": "Qwen3.6 27B" }, - { "id": "qwen36-27b-fast", "name": "Qwen3.6 27B Fast" } - ] - } - } - } -}`; - const authProfile = `{ - "profiles": { - "runpod:nextbase-gateway": { - "type": "api_key", - "provider": "runpod", - "key": "${token}" - } - } -}`; - const verifyCurl = `curl -sS -i \ - -H "Authorization: Bearer ${token}" \ - -H "Content-Type: application/json" \ - -d '{"model":"qwen36-27b-fast","messages":[{"role":"user","content":"pong"}],"max_tokens":16}' \ - ${baseUrl}/v1/runpod/chat/completions`; - const modelsCurl = `curl -sS \ - -H "Authorization: Bearer ${token}" \ - ${baseUrl}/v1/runpod/models`; - return setupProviderBlock({ - desc: 'Runpod Serverless Qwen is exposed as OpenAI-compatible chat under /v1/runpod. Non-admin users are disabled by default; enable Runpod per user in Model access when needed.', - providerJson, authProfile, verifyCurl, - agentLine: buildAgentSetupPrompt(baseUrl, 'runpod', token), - verifyNote: 'Expect HTTP/2 200 with x-gateway-provider: runpod. If you get model_not_allowed_for_user, enable Runpod for that user first.', - extra: el('div', { class: 'mt-3' }, - el('div', { class: 'setup-block-label' }, 'List models'), - setupCodeBlock(modelsCurl)), - }); - }, - - aside({ baseUrl, token }) { - const taskCurl = `curl -sS -i \ - -H "Authorization: Bearer ${token}" \ - -H "Content-Type: application/json" \ - -d '{"prompt":"Open Gmailnator, create a temporary Gmail address, and return only the email address.","timeoutMs":600000}' \ - ${baseUrl}/api/unsupported/aside/task`; - const mcpEndpoint = `${baseUrl}/api/unsupported/aside/mcp`; - const taskBody = `{ - "prompt": "Describe the browser task to run in your assigned Aside profile.", - "timeoutMs": 600000, - "useExec": false, - "model": "optional model hint", - "provider": "optional provider hint", - "speed": "default", - "effort": "minimal" -}`; - return el('div', { class: 'stack' }, - el('p', { class: 'fs-13 text-muted', style: { lineHeight: '1.55' } }, - 'Aside runs browser automation on your assigned Mac profile through Super Proxy. Clients authenticate with their normal ', - el('code', { class: 'mono' }, 'sp_*'), - ' API token; Super Proxy resolves the user assignment and forwards to the right Mac gateway server-side.'), - el('div', { class: 'setup-block-label' }, 'Task endpoint'), - setupCodeBlock(`POST ${baseUrl}/api/unsupported/aside/task`), - el('p', { class: 'fs-12 text-muted', style: { lineHeight: '1.5' } }, - 'Use for one-shot browser tasks. Required field: ', el('code', { class: 'mono' }, 'prompt'), - '. Optional fields: ', el('code', { class: 'mono' }, 'timeoutMs'), ', ', - el('code', { class: 'mono' }, 'useExec'), ', ', el('code', { class: 'mono' }, 'model'), ', ', - el('code', { class: 'mono' }, 'provider'), ', ', el('code', { class: 'mono' }, 'speed'), ', ', - el('code', { class: 'mono' }, 'effort'), '.'), - el('div', { class: 'setup-block-label mt-3' }, 'Task request body'), - setupCodeBlock(taskBody), - el('div', { class: 'setup-block-label mt-3' }, 'Task curl example'), - setupCodeBlock(taskCurl), - el('div', { class: 'setup-block-label mt-3' }, 'MCP endpoint'), - setupCodeBlock(mcpEndpoint), - el('p', { class: 'fs-12 text-muted', style: { lineHeight: '1.5' } }, - 'Point Streamable HTTP MCP clients at this Super Proxy URL with the same Bearer token. Super Proxy owns the per-user Mac routing.'), - el('div', { class: 'notice notice--warn mt-3' }, - el('strong', {}, 'Do not call Mac gateway URLs directly from clients.'), - ' Mac endpoints such as ', el('code', { class: 'mono' }, '/v1/profiles'), ', ', - el('code', { class: 'mono' }, '/v1/profiles/:label/tasks'), ', and ', - el('code', { class: 'mono' }, '/v1/profiles/:label/mcp'), - ' are admin/internal surfaces used by Super Proxy with stored gateway service tokens.'), - ); - }, - - manual({ baseUrl, token }) { - const providerBundle = `{ - "models": { - "providers": { - "anthropic": { - "baseUrl": "${baseUrl}", - "authHeader": true, - "models": [] - }, - "openai-codex": { - "baseUrl": "${baseUrl}/v1", - "api": "openai-responses", - "apiKey": "${token}", - "models": [ - { - "id": "gpt-5.5", - "name": "GPT-5.5", - "reasoning": true, - "input": ["text", "image"], - "contextWindow": 400000, - "maxTokens": 128000 - } - ] - }, - "openai": { - "baseUrl": "${baseUrl}/v1", - "apiKey": "${token}", - "models": [] - }, - "groq": { - "baseUrl": "${baseUrl}/v1/groq", - "apiKey": "${token}", - "models": [ - { "id": "openai/gpt-oss-120b", "name": "Groq GPT OSS 120B" } - ] - }, - "cerebras": { - "baseUrl": "${baseUrl}/v1/cerebras", - "apiKey": "${token}", - "models": [ - { "id": "gpt-oss-120b", "name": "Cerebras GPT OSS 120B" }, - { "id": "zai-glm-4.7", "name": "Cerebras Z.ai GLM 4.7" } - ] - }, - "kimi": { - "baseUrl": "${baseUrl}/v1/kimi", - "apiKey": "${token}", - "models": [ - { "id": "k3", "name": "Kimi K3" }, - { "id": "kimi-k2.7-code", "name": "Kimi K2.7 Code" }, - { "id": "kimi-k2.6", "name": "Kimi K2.6" }, - { "id": "kimi-for-coding", "name": "Kimi for Coding" } - ] - }, - "kimi-anthropic": { - "baseUrl": "${baseUrl}/v1/kimi", - "authHeader": true, - "models": [] - }, - "glm": { - "baseUrl": "${baseUrl}/v1/glm", - "api": "anthropic-messages", - "authHeader": true, - "apiKey": "${token}", - "models": [ - { "id": "glm-5.2", "name": "GLM 5.2" } - ] - }, - "xai": { - "baseUrl": "${baseUrl}/v1/xai", - "api": "openai-responses", - "apiKey": "***", - "models": [ - { "id": "grok-4.3", "name": "Grok 4.3", "reasoning": true, "input": ["text", "image"] } - ] - } - } - } -}`; - const restart = 'systemctl restart openclaw-gateway.service'; - return el('div', { class: 'stack' }, - el('p', { class: 'fs-13 text-muted', style: { lineHeight: '1.55' } }, - 'Paste this complete bundle into ', - el('code', { class: 'mono' }, '~/.openclaw/openclaw.json'), ' and ', - el('code', { class: 'mono' }, '~/.openclaw/agents/main/agent/models.json'), - ', then drop the matching profiles into ', - el('code', { class: 'mono' }, '~/.openclaw/agents/main/agent/auth-profiles.json'), '.'), - el('div', { class: 'setup-block-label' }, 'Complete provider bundle'), - setupCodeBlock(providerBundle), - el('div', { class: 'setup-block-label mt-3' }, 'Restart the gateway'), - setupCodeBlock(restart), - el('div', { class: 'notice notice--info mt-3' }, - 'Pass an ', el('code', { class: 'mono' }, 'x-conversation-id'), ' header to keep a single conversation pinned to the same upstream account — better prompt-cache reuse, lower spend.'), - ); - }, - - fusion({ baseUrl, token }) { - const desc = 'Fusion sends your prompt to multiple models in parallel, then a synthesizer compares their responses and writes a better final answer. No extra provider config needed — fusion works through the same sp_* token you already have.'; - - const presetsTable = el('table', { class: 'tbl tbl-collapse', style: { fontSize: '12px' } }, - el('thead', {}, el('tr', {}, - el('th', {}, 'Model alias'), - el('th', {}, 'Panel models'), - el('th', {}, 'Synthesizer'), - el('th', {}, 'Cost'), - )), - el('tbody', {}, - el('tr', {}, - el('td', {}, el('code', { class: 'mono' }, 'fusion/max')), - el('td', { class: 'text-muted' }, 'Claude Opus 4.8 + GPT-5.5 + Tencent HY3 Free'), - el('td', { class: 'text-muted' }, 'Claude Opus 4.8'), - el('td', { class: 'text-muted' }, '~$0.20\u2013$0.50+'), - ), - el('tr', {}, - el('td', {}, el('code', { class: 'mono' }, 'fusion/quality')), - el('td', { class: 'text-muted' }, 'Claude Opus 4.8 + GPT-5.5 + Tencent HY3 Free'), - el('td', { class: 'text-muted' }, 'Claude Opus 4.8'), - el('td', { class: 'text-muted' }, '~$0.10\u2013$0.30'), - ), - el('tr', {}, - el('td', {}, el('code', { class: 'mono' }, 'fusion/budget')), - el('td', { class: 'text-muted' }, 'Claude Sonnet + GPT-5.4 + Tencent HY3 Free'), - el('td', { class: 'text-muted' }, 'Claude Sonnet'), - el('td', { class: 'text-muted' }, '~$0.03\u2013$0.08'), - ), - el('tr', {}, - el('td', {}, el('code', { class: 'mono' }, 'fusion/custom')), - el('td', { class: 'text-muted' }, 'Your choice (1\u20138)'), - el('td', { class: 'text-muted' }, 'Your choice'), - el('td', { class: 'text-muted' }, 'Varies'), - ), - el('tr', {}, - el('td', {}, el('code', { class: 'mono' }, 'fusion/')), - el('td', { class: 'text-muted' }, 'Saved via dashboard'), - el('td', { class: 'text-muted' }, 'Saved via dashboard'), - el('td', { class: 'text-muted' }, 'Varies'), - ), - ), - ); - - const synthesizeCurl = `curl -sS -N \\ - -H "Authorization: Bearer ${token}" \\ - -H "Content-Type: application/json" \\ - -d '{ - "model": "fusion/quality", - "stream": true, - "messages": [{"role": "user", "content": "Compare microservices vs monolith for a 5-person startup."}] - }' \\ - ${baseUrl}/v1/fusion/chat/completions`; - - const compareCurl = `curl -sS \\ - -H "Authorization: Bearer ${token}" \\ - -H "Content-Type: application/json" \\ - -d '{ - "model": "fusion/quality", - "messages": [{"role": "user", "content": "Explain quantum computing."}], - "fusion": {"mode": "compare"} - }' \\ - ${baseUrl}/v1/fusion/chat/completions`; - - const customCurl = `curl -sS \\ - -H "Authorization: Bearer ${token}" \\ - -H "Content-Type: application/json" \\ - -d '{ - "model": "fusion/custom", - "messages": [{"role": "user", "content": "Review this design."}], - "fusion": { - "panel": ["anthropic/claude-sonnet-4-5-20250929", "xai/grok-4-fast"], - "synthesizer": "anthropic/claude-sonnet-4-5-20250929" - } - }' \\ - ${baseUrl}/v1/fusion/chat/completions`; - - const modelsCurl = `curl -sS -H "Authorization: Bearer ${token}" \\ - ${baseUrl}/v1/models`; - - return el('div', { class: 'stack' }, - setupAgentCard(`Read ${baseUrl}/skills/fusion/SKILL.md and set me up for Fusion multi-model deliberation. My sp_* token is ${token}.`), - el('p', { class: 'fs-13 text-muted', style: { lineHeight: '1.55' } }, desc), - - el('div', { class: 'setup-block-label' }, 'Available presets'), - el('div', { class: 'table-wrap' }, presetsTable), - - el('div', { class: 'setup-block-label mt-3' }, 'Synthesize mode ', el('span', { class: 'text-muted fs-12' }, '\u2014 best answer from multiple models')), - setupCodeBlock(synthesizeCurl), - - el('div', { class: 'setup-block-label mt-3' }, 'Compare mode ', el('span', { class: 'text-muted fs-12' }, '\u2014 see each model\u2019s raw response side-by-side')), - setupCodeBlock(compareCurl), - - el('div', { class: 'setup-block-label mt-3' }, 'Custom panel ', el('span', { class: 'text-muted fs-12' }, '\u2014 pick your own models')), - setupCodeBlock(customCurl), - - el('div', { class: 'setup-block-label mt-3' }, 'Model discovery'), - el('p', { class: 'fs-12 text-muted', style: { lineHeight: '1.5' } }, - 'Fusion presets appear in ', el('code', { class: 'mono' }, 'GET /v1/models'), - ' so clients like Cursor can discover them. Your saved custom presets are included too.'), - setupCodeBlock(modelsCurl), - - el('div', { class: 'setup-block-label mt-3' }, 'Custom presets'), - el('p', { class: 'fs-12 text-muted', style: { lineHeight: '1.5' } }, - 'Create and manage custom presets in the ', - el('a', { href: '#fusion', style: { color: 'var(--accent)' } }, 'Fusion dashboard'), - '. Saved presets are usable as ', el('code', { class: 'mono' }, 'model: "fusion/"'), - ' from any client.'), - - el('div', { class: 'setup-block-label mt-3' }, 'OCPlatform setup'), - el('p', { class: 'fs-12 text-muted', style: { lineHeight: '1.5' } }, - 'Fusion needs a provider block in ', - el('code', { class: 'mono' }, 'openclaw.json'), - ' / ', - el('code', { class: 'mono' }, 'models.json'), - ' with ', el('code', { class: 'mono' }, 'api: "openai-completions"'), - ' and ', el('code', { class: 'mono' }, 'baseUrl'), - ' ending in ', el('code', { class: 'mono' }, '/v1/fusion'), - '. You also need to add fusion models to ', - el('code', { class: 'mono' }, 'agents.defaults.models'), - ' or they won\u2019t appear in ', el('code', { class: 'mono' }, '/models'), - '. See the ', el('a', { href: `${baseUrl}/skills/fusion/SKILL.md`, style: { color: 'var(--accent)' } }, 'fusion skill guide'), - ' for full instructions. The one-shot setup script above handles all of this automatically.'), - ); - }, - - troubleshoot() { - const item = (q, a) => el('li', {}, - el('div', { class: 'setup-troubleshoot__q' }, q), - el('div', { class: 'setup-troubleshoot__a fs-13 text-muted' }, a), - ); - return el('ul', { class: 'setup-troubleshoot' }, - item( - '401 “Invalid or disabled API token” when running the token check', - el('span', {}, - 'The token in your Authorization header doesn\'t match a live row in this gateway. Causes: you pasted the ', - el('code', { class: 'mono' }, 'token_prefix'), - ' shown in the dashboard (only the first ~12 chars) instead of the full ', - el('code', { class: 'mono' }, 'sp_*'), ' secret saved at creation time; the token was disabled or deleted; or you typed it into the wrong field. Create a new token and re-run the check.'), - ), - item( - '401 “invalid x-api-key” in usage logs', - el('span', {}, - 'OCPlatform is sending Anthropic-style ', el('code', { class: 'mono' }, 'x-api-key'), - ' instead of Bearer. Set ', el('code', { class: 'mono' }, 'authHeader: true'), - ' on the Anthropic (and ', el('code', { class: 'mono' }, 'kimi-anthropic'), - ') provider block so OCPlatform uses ', el('code', { class: 'mono' }, 'Authorization: Bearer …'), - '. The gateway accepts both, but Bearer is the canonical shape.'), - ), - item( - '401 / 403 with “IP not allowed” or similar', - 'Your token has an IP allow-list on it and your machine isn\'t in the list. Either disable the IP restriction on the token in the dashboard, or issue a new unrestricted token for this machine.', - ), - item( - 'Codex still hits ChatGPT directly instead of the gateway', - el('span', {}, - 'The native Codex runtime in OCPlatform (', - el('code', { class: 'mono' }, 'agentRuntime.id: "codex"'), - ') talks straight to ChatGPT and ignores any HTTP base URL. To route through this gateway, use the ', - el('code', { class: 'mono' }, 'openai-codex'), ' provider with ', - el('code', { class: 'mono' }, 'api: "openai-responses"'), ' and ', - el('code', { class: 'mono' }, 'baseUrl: /v1'), - ', and pick a ', el('code', { class: 'mono' }, 'openai-codex/*'), ' model in your agent.'), - ), - item( - '“missing Authorization header” / connection refused on token check', - 'Your curl probably split lines wrong, or the URL doesn\'t include /v1/token/check. Copy the verify curl from the hero card above and re-run it verbatim.', - ), - item( - 'Image generation returns 404 or “unknown model”', - el('span', {}, - 'You configured ', el('code', { class: 'mono' }, 'openai-codex'), - ' but not the regular ', el('code', { class: 'mono' }, 'openai'), - ' provider. OCPlatform\'s image plugin uses the plain OpenAI provider for ', - el('code', { class: 'mono' }, 'gpt-image-2'), - ' — see the Images tab.'), - ), - item( - 'Upstream account doesn\'t stay sticky across turns', - el('span', {}, - 'Set an ', el('code', { class: 'mono' }, 'x-conversation-id'), - ' header on every request in the same conversation. Without it the governor will load-balance freely and lose prompt-cache hits.'), - ), - ); - }, -}; - -/* ─── One-shot setup script (Step 2) ───────────────────────── */ -function buildOneShotScript(baseUrl, token) { - return `#!/usr/bin/env bash -set -euo pipefail - -export TOKEN="${token}" -export BASE_URL="${baseUrl}" - -if [ "$TOKEN" = "" ] || [ -z "$TOKEN" ]; then - echo "Edit TOKEN first: replace with your sp_* token" >&2 - exit 1 -fi - -node <<'NODE' -const fs = require('fs'); -const os = require('os'); -const path = require('path'); - -const token = process.env.TOKEN; -const baseUrl = process.env.BASE_URL; -const home = os.homedir(); -const stamp = new Date().toISOString().replace(/[-:T.Z]/g, '').slice(0, 14); - -function readJson(file, fallback) { - try { - const raw = fs.readFileSync(file, 'utf8').trim(); - return raw ? JSON.parse(raw) : fallback; - } catch (_) { - return fallback; - } -} -function writeJson(file, data) { - fs.mkdirSync(path.dirname(file), { recursive: true }); - if (fs.existsSync(file)) fs.copyFileSync(file, file + '.bak-nextbase-' + stamp); - fs.writeFileSync(file, JSON.stringify(data, null, 2) + '\\n'); - console.log('patched ' + file); -} - -const anthropic = { baseUrl, authHeader: true, models: [] }; -const codex = { - baseUrl: baseUrl + '/v1', - api: 'openai-responses', - apiKey: token, - models: [{ id: 'gpt-5.5', name: 'GPT-5.5', reasoning: true, input: ['text', 'image'], contextWindow: 400000, maxTokens: 128000 }] -}; -const openai = { baseUrl: baseUrl + '/v1', apiKey: token, models: [] }; -const groq = { baseUrl: baseUrl + '/v1/groq', apiKey: token, models: [{ id: 'openai/gpt-oss-120b', name: 'Groq GPT OSS 120B' }] }; -const cerebras = { baseUrl: baseUrl + '/v1/cerebras', apiKey: token, models: [{ id: 'gpt-oss-120b', name: 'Cerebras GPT OSS 120B' }, { id: 'zai-glm-4.7', name: 'Cerebras Z.ai GLM 4.7' }] }; -const kimi = { baseUrl: baseUrl + '/v1/kimi', apiKey: token, models: [{ id: 'k3', name: 'Kimi K3' }, { id: 'kimi-k2.7-code', name: 'Kimi K2.7 Code' }, { id: 'kimi-k2.6', name: 'Kimi K2.6' }, { id: 'kimi-for-coding', name: 'Kimi for Coding' }] }; -const kimiAnthropic = { baseUrl: baseUrl + '/v1/kimi', authHeader: true, models: [] }; -const glm = { baseUrl: baseUrl + '/v1/glm', api: 'anthropic-messages', authHeader: true, apiKey: token, models: [{ id: 'glm-5.2', name: 'GLM 5.2' }] }; -const openrouter = { baseUrl: baseUrl + '/v1/openrouter', apiKey: token, models: [{ id: 'tencent/hy3:free', name: 'Tencent HY3 Free' }] }; -const deepgram = { baseUrl: baseUrl + '/v1/deepgram', apiKey: token, models: [{ id: 'nova-3', name: 'Deepgram Nova-3' }, { id: 'nova-2', name: 'Deepgram Nova-2' }, { id: 'whisper', name: 'Deepgram Whisper Cloud' }] }; -const xai = { baseUrl: baseUrl + '/v1/xai', api: 'openai-responses', apiKey: token, models: [{ id: 'grok-4.3', name: 'Grok 4.3', reasoning: true, input: ['text', 'image'] }] }; -const runpod = { baseUrl: baseUrl + '/v1/runpod', apiKey: token, models: [{ id: 'qwen36-27b', name: 'Qwen3.6 27B' }, { id: 'qwen36-27b-fast', name: 'Qwen3.6 27B Fast' }] }; - -for (const file of [ - path.join(home, '.openclaw/openclaw.json'), - path.join(home, '.openclaw/agents/main/agent/models.json'), -]) { - const data = readJson(file, {}); - const providers = file.endsWith('openclaw.json') - ? (((data.models ||= {}).providers ||= {})) - : (data.providers ||= {}); - providers.anthropic = anthropic; - providers['openai-codex'] = codex; - providers.openai = openai; - providers.groq = groq; - providers.cerebras = cerebras; - providers.kimi = kimi; - providers['kimi-anthropic'] = kimiAnthropic; - providers.glm = glm; - providers.openrouter = openrouter; - providers.deepgram = deepgram; - providers.xai = xai; - providers.runpod = runpod; - writeJson(file, data); -} - -const profilesFile = path.join(home, '.openclaw/agents/main/agent/auth-profiles.json'); -const profilesData = readJson(profilesFile, { profiles: {} }); -const profiles = profilesData.profiles ||= {}; -profiles['anthropic:manual'] = { type: 'token', provider: 'anthropic', token }; -profiles['openai-codex:nextbase-gateway'] = { type: 'api_key', provider: 'openai-codex', key: token }; -profiles['openai:nextbase-gateway'] = { type: 'api_key', provider: 'openai', key: token }; -profiles['groq:nextbase-gateway'] = { type: 'api_key', provider: 'groq', key: token }; -profiles['cerebras:nextbase-gateway'] = { type: 'api_key', provider: 'cerebras', key: token }; -profiles['kimi:nextbase-gateway'] = { type: 'api_key', provider: 'kimi', key: token }; -profiles['kimi-anthropic:nextbase-gateway'] = { type: 'token', provider: 'kimi-anthropic', token }; -profiles['glm:manual'] = { type: 'token', provider: 'glm', token }; -profiles['openrouter:nextbase-gateway'] = { type: 'api_key', provider: 'openrouter', key: token }; -profiles['deepgram:nextbase-gateway'] = { type: 'api_key', provider: 'deepgram', key: token }; -profiles['xai:nextbase-gateway'] = { type: 'api_key', provider: 'xai', key: token }; -profiles['runpod:nextbase-gateway'] = { type: 'api_key', provider: 'runpod', key: token }; -writeJson(profilesFile, profilesData); - -const stateFile = path.join(home, '.openclaw/agents/main/agent/auth-state.json'); -const state = readJson(stateFile, {}); -state.lastGood ||= {}; -Object.assign(state.lastGood, { - anthropic: 'anthropic:manual', - 'openai-codex': 'openai-codex:nextbase-gateway', - openai: 'openai:nextbase-gateway', - groq: 'groq:nextbase-gateway', - cerebras: 'cerebras:nextbase-gateway', - kimi: 'kimi:nextbase-gateway', - 'kimi-anthropic': 'kimi-anthropic:nextbase-gateway', - glm: 'glm:manual', - openrouter: 'openrouter:nextbase-gateway', - deepgram: 'deepgram:nextbase-gateway', - xai: 'xai:nextbase-gateway', - runpod: 'runpod:nextbase-gateway', -}); -state.order ||= {}; -for (const [provider, profile] of Object.entries(state.lastGood)) { - const old = Array.isArray(state.order[provider]) ? state.order[provider] : []; - state.order[provider] = [profile, ...old.filter(x => x !== profile)]; -} -writeJson(stateFile, state); -NODE - -if command -v systemctl >/dev/null 2>&1 && systemctl list-unit-files openclaw-gateway.service >/dev/null 2>&1; then - systemctl restart openclaw-gateway.service - systemctl is-active openclaw-gateway.service -else - echo "Restart OCPlatform now (gateway service not found)." -fi -`; -} /* ══════════════════════════════════════════════════════════════════ ▓▓▓▓▓ FUSION (dev + admin) ▓▓▓▓▓ ══════════════════════════════════════════════════════════════════ */ @@ -5639,7 +4078,7 @@ function renderMonitoring() { providersHost.replaceChildren(card('Providers', providerTable(overview.providers), `${overview.providers.length} active`)); cacheHost.replaceChildren(card('Cache efficiency by model', cacheTable(cache.rows))); - poolsHost.replaceChildren(card('Pool health', poolTable(reliability), `${reliability.inFlightTotal} in flight`), card('Subscription utilization', utilTable(utilization.accounts))); + poolsHost.replaceChildren(card('Pool health', poolTable(reliability), `${reliability.inFlightTotal} in flight`), card('Account utilization', utilTable(utilization.accounts))); topHost.replaceChildren(card('Top consumers', topTable(top))); burnHost.replaceChildren( card(`Metered burn · ${burn.month}`, burnBody(burn), 'Month to date — not affected by range filter'), @@ -5846,14 +4285,4 @@ document.addEventListener('DOMContentLoaded', () => { }, 10000); }); - -/* ──────────────── Aside (browser automation) ──────────────── */ - -async function renderAside() { - return el('div', { class: 'panel' }, - el('h2', { class: 'panel__title' }, 'Aside'), - el('p', { class: 'muted' }, 'Aside browser-automation control plane is not included in Super Proxy OSS builds.') - ); -} - })(); diff --git a/public/index.html b/public/index.html index da41423..7481b96 100644 --- a/public/index.html +++ b/public/index.html @@ -22,7 +22,7 @@ Model Gateway

Sign in to the console

-

Internal AI proxy operations. Use your allowlisted Google account.

+

Manage your self-hosted AI gateway. Use an authorized Google account.

Dev fallback: set DEV_ADMIN_KEY and paste it after login if needed.
-
-

Super Proxy

Internal AI proxy ops dashboard
-
-

Health

loading…
-

Create User

-

Create Token

-

Limits

-

Test as User

-
- -
-

Provider Accounts

Anthropic / Codex / OpenAI backend accounts
- - -
- -

Users

-

Limits

-

Usage 24h

-

Alerts

-

Audit Logs

-
-
Copied
- - - diff --git a/public/skills/SKILL.md b/public/skills/SKILL.md deleted file mode 100644 index b0df7b0..0000000 --- a/public/skills/SKILL.md +++ /dev/null @@ -1,246 +0,0 @@ ---- -name: nextbase-super-proxy-setup -description: Set up OCPlatform or another agent client to use all Super Proxy providers. ---- - -# Super Proxy - -Super Proxy is a proxy in front of multiple model providers, authenticated by a single `sp_*` token. It lets OCPlatform, Claude Code, Codex CLI, Kimi CLI, Hermes, and other LLM-driven agents route model calls through Nextbase without storing upstream provider secrets locally. The gateway validates your token, selects a healthy upstream account, enforces caps, and returns provider-compatible responses. - -## Clients - -Pick your agent client first — the wiring differs by client, the providers are the same: - -| Client | Config style | Setup | -| --- | --- | --- | -| OCPlatform | JSON (`openclaw.json` / `models.json` / auth profiles), `authHeader: true` | the per-provider skills below | -| Hermes (NousResearch/hermes-agent) | YAML `config.yaml` `custom_providers` + `key_env` env token | [/skills/hermes/SKILL.md](/skills/hermes/SKILL.md) | - -The Anthropic billing path is detected automatically per client (Hermes `mcp_` tools → Hermes transform; OCPlatform bare tools → legacy transform) — no client flag to set. - -## Providers - -| Provider | Routes | Skill | -| --- | --- | --- | -| Anthropic | `/v1/messages`, `/v1/messages/count_tokens` | [/skills/anthropic/SKILL.md](/skills/anthropic/SKILL.md) | -| OpenAI Codex | `/v1/responses`, `/v1/realtime/client_secrets` | [/skills/openai-codex/SKILL.md](/skills/openai-codex/SKILL.md) | -| OpenAI Images | `/v1/images/generations`, `/v1/images/edits` | [/skills/openai-images/SKILL.md](/skills/openai-images/SKILL.md) | -| Groq | `/v1/groq/chat/completions`, `/v1/groq/audio/transcriptions`, `/v1/groq/audio/translations` | [/skills/groq/SKILL.md](/skills/groq/SKILL.md) | -| Cerebras | `/v1/cerebras/chat/completions` | [/skills/cerebras/SKILL.md](/skills/cerebras/SKILL.md) | -| Kimi | `/v1/kimi/chat/completions`, `/v1/kimi/messages` | [/skills/kimi/SKILL.md](/skills/kimi/SKILL.md) | -| GLM (z.ai) | `/v1/glm/messages` | [/skills/glm/SKILL.md](/skills/glm/SKILL.md) | -| Gemini | `/v1/gemini/embeddings`, `/v1/gemini/chat/completions`, `/v1/gemini/tts` | [/skills/gemini/SKILL.md](/skills/gemini/SKILL.md) | -| OpenRouter | `/v1/openrouter/chat/completions` | [/skills/openrouter/SKILL.md](/skills/openrouter/SKILL.md) | -| Deepgram | `/v1/deepgram/listen` | [/skills/deepgram/SKILL.md](/skills/deepgram/SKILL.md) | -| xAI | `/v1/xai/responses`, `/v1/xai/realtime/client_secrets`, `/v1/xai/tts`, `/v1/xai/stt`, `/v1/xai/images/generations`, `/v1/xai/videos/generations`, `/v1/xai/videos/:requestId` | [/skills/xai/SKILL.md](/skills/xai/SKILL.md) | -| Runpod | `/v1/runpod/chat/completions`, `/v1/runpod/models` | [/skills/runpod/SKILL.md](/skills/runpod/SKILL.md) | -| Search | `/v1/search` | [/skills/search/SKILL.md](/skills/search/SKILL.md) | -| Fusion | `/v1/fusion/chat/completions`, `/v1/models` | [/skills/fusion/SKILL.md](/skills/fusion/SKILL.md) | - -## One-shot setup script - -This script patches the standard OCPlatform config files with all Nextbase providers, writes auth profiles using your `sp_*` token, updates `auth-state.json` `lastGood` selections, creates timestamped backups, and restarts `openclaw-gateway.service` when present. Replace `` with the full token value; do not use only the dashboard token prefix. - -```bash -#!/usr/bin/env bash -set -euo pipefail - -export TOKEN="" -export BASE_URL="http://localhost:8080" - -if [ "$TOKEN" = "" ] || [ -z "$TOKEN" ]; then - echo "Edit TOKEN first: replace with your sp_* token" >&2 - exit 1 -fi - -node <<'NODE' -const fs = require('fs'); -const os = require('os'); -const path = require('path'); - -const token = process.env.TOKEN; -const baseUrl = process.env.BASE_URL; -const home = os.homedir(); -const stamp = new Date().toISOString().replace(/[-:T.Z]/g, '').slice(0, 14); - -function readJson(file, fallback) { - try { - const raw = fs.readFileSync(file, 'utf8').trim(); - return raw ? JSON.parse(raw) : fallback; - } catch (_) { - return fallback; - } -} -function writeJson(file, data) { - fs.mkdirSync(path.dirname(file), { recursive: true }); - if (fs.existsSync(file)) fs.copyFileSync(file, file + '.bak-nextbase-' + stamp); - fs.writeFileSync(file, JSON.stringify(data, null, 2) + '\n'); - console.log('patched ' + file); -} - -const anthropic = { baseUrl, authHeader: true, models: [] }; -const codex = { - baseUrl: baseUrl + '/v1', - api: 'openai-responses', - apiKey: token, - models: [{ id: 'gpt-5.5', name: 'GPT-5.5', reasoning: true, input: ['text', 'image'], contextWindow: 400000, maxTokens: 128000 }] -}; -const openai = { baseUrl: baseUrl + '/v1', apiKey: token, models: [] }; -const groq = { baseUrl: baseUrl + '/v1/groq', apiKey: token, models: [{ id: 'openai/gpt-oss-120b', name: 'Groq GPT OSS 120B' }] }; -const cerebras = { baseUrl: baseUrl + '/v1/cerebras', apiKey: token, models: [{ id: 'gpt-oss-120b', name: 'Cerebras GPT OSS 120B' }, { id: 'zai-glm-4.7', name: 'Cerebras Z.ai GLM 4.7' }] }; -const kimi = { baseUrl: baseUrl + '/v1/kimi', apiKey: token, models: [{ id: 'k3', name: 'Kimi K3' }, { id: 'kimi-k2.7-code', name: 'Kimi K2.7 Code' }, { id: 'kimi-k2.6', name: 'Kimi K2.6' }, { id: 'kimi-for-coding', name: 'Kimi for Coding' }] }; -// Native Gemini free-tier pool (OpenAI-compat). Primary use is pooled embeddings for memorySearch; chat is optional. -const geminiNextbase = { baseUrl: baseUrl + '/v1/gemini', apiKey: token, models: [{ id: 'gemini-3.1-flash-lite', name: 'Gemini 3.1 Flash Lite' }] }; -const kimiAnthropic = { baseUrl: baseUrl + '/v1/kimi', authHeader: true, models: [] }; -// GLM (z.ai) is a transparent Anthropic Messages passthrough, so use authHeader: true (Bearer), like kimi-anthropic. -const glm = { baseUrl: baseUrl + '/v1/glm', authHeader: true, apiKey: token, models: [{ id: 'glm-5.2', name: 'GLM 5.2' }, { id: 'glm-5-turbo', name: 'GLM 5 Turbo' }, { id: 'glm-4.7', name: 'GLM 4.7' }] }; -const openrouter = { - baseUrl: baseUrl + '/v1/openrouter', - apiKey: token, - models: [ - { id: 'tencent/hy3:free', name: 'Tencent HY3 Free' }, - ], -}; -const deepgram = { - baseUrl: baseUrl + '/v1/deepgram', - apiKey: token, - models: [ - { id: 'nova-3', name: 'Deepgram Nova-3' }, - { id: 'nova-2', name: 'Deepgram Nova-2' }, - { id: 'whisper', name: 'Deepgram Whisper Cloud' }, - ], -}; -const xai = { - baseUrl: baseUrl + '/v1/xai', - api: 'openai-responses', - apiKey: token, - models: [{ id: 'grok-4.3', name: 'Grok 4.3', reasoning: true, input: ['text', 'image'] }], -}; -const runpod = { - baseUrl: baseUrl + '/v1/runpod', - apiKey: token, - models: [ - { id: 'qwen36-27b', name: 'Qwen3.6 27B' }, - { id: 'qwen36-27b-fast', name: 'Qwen3.6 27B Fast' }, - ], -}; -const fusion = { - baseUrl: baseUrl + '/v1/fusion', - apiKey: token, - api: 'openai-completions', - models: [ - { id: 'fusion/max', name: 'Fusion Max', reasoning: true, input: ['text'], contextWindow: 200000, maxTokens: 8192 }, - { id: 'fusion/quality', name: 'Fusion Quality', reasoning: true, input: ['text'], contextWindow: 200000, maxTokens: 8192 }, - { id: 'fusion/budget', name: 'Fusion Budget', reasoning: true, input: ['text'], contextWindow: 200000, maxTokens: 8192 }, - ], -}; - -for (const file of [ - path.join(home, '.openclaw/openclaw.json'), - path.join(home, '.openclaw/agents/main/agent/models.json'), -]) { - const data = readJson(file, {}); - const providers = file.endsWith('openclaw.json') - ? (((data.models ||= {}).providers ||= {})) - : (data.providers ||= {}); - providers.anthropic = anthropic; - providers['openai-codex'] = codex; - providers.openai = openai; - providers.groq = groq; - providers.cerebras = cerebras; - providers.kimi = kimi; - providers['kimi-anthropic'] = kimiAnthropic; - providers.glm = glm; - providers['gemini-nextbase'] = geminiNextbase; - providers.openrouter = openrouter; - providers.deepgram = deepgram; - providers.xai = xai; - providers.runpod = runpod; - providers.fusion = fusion; - // Patch agents.defaults.models so /models command lists fusion models - if (file.endsWith('openclaw.json')) { - const models = ((data.agents ||= {}).defaults ||= {}).models ||= {}; - models['fusion/fusion/max'] = models['fusion/fusion/max'] || {}; - models['fusion/fusion/quality'] = models['fusion/fusion/quality'] || {}; - models['fusion/fusion/budget'] = models['fusion/fusion/budget'] || {}; - } - writeJson(file, data); -} - -const profilesFile = path.join(home, '.openclaw/agents/main/agent/auth-profiles.json'); -const profilesData = readJson(profilesFile, { profiles: {} }); -const profiles = profilesData.profiles ||= {}; -profiles['anthropic:manual'] = { type: 'token', provider: 'anthropic', token }; -profiles['openai-codex:nextbase-gateway'] = { type: 'api_key', provider: 'openai-codex', key: token }; -profiles['openai:nextbase-gateway'] = { type: 'api_key', provider: 'openai', key: token }; -profiles['groq:nextbase-gateway'] = { type: 'api_key', provider: 'groq', key: token }; -profiles['cerebras:nextbase-gateway'] = { type: 'api_key', provider: 'cerebras', key: token }; -profiles['kimi:nextbase-gateway'] = { type: 'api_key', provider: 'kimi', key: token }; -profiles['kimi-anthropic:nextbase-gateway'] = { type: 'token', provider: 'kimi-anthropic', token }; -profiles['glm:nextbase-gateway'] = { type: 'token', provider: 'glm', token }; -profiles['openrouter:nextbase-gateway'] = { type: 'api_key', provider: 'openrouter', key: token }; -profiles['deepgram:nextbase-gateway'] = { type: 'api_key', provider: 'deepgram', key: token }; -profiles['xai:nextbase-gateway'] = { type: 'api_key', provider: 'xai', key: token }; -profiles['runpod:nextbase-gateway'] = { type: 'api_key', provider: 'runpod', key: token }; -profiles['fusion:nextbase-gateway'] = { type: 'api_key', provider: 'fusion', key: token }; -writeJson(profilesFile, profilesData); - -const stateFile = path.join(home, '.openclaw/agents/main/agent/auth-state.json'); -const state = readJson(stateFile, {}); -state.lastGood ||= {}; -Object.assign(state.lastGood, { - anthropic: 'anthropic:manual', - 'openai-codex': 'openai-codex:nextbase-gateway', - openai: 'openai:nextbase-gateway', - groq: 'groq:nextbase-gateway', - cerebras: 'cerebras:nextbase-gateway', - kimi: 'kimi:nextbase-gateway', - 'kimi-anthropic': 'kimi-anthropic:nextbase-gateway', - glm: 'glm:nextbase-gateway', - openrouter: 'openrouter:nextbase-gateway', - deepgram: 'deepgram:nextbase-gateway', - xai: 'xai:nextbase-gateway', - runpod: 'runpod:nextbase-gateway', - fusion: 'fusion:nextbase-gateway', -}); -state.order ||= {}; -for (const [provider, profile] of Object.entries(state.lastGood)) { - const old = Array.isArray(state.order[provider]) ? state.order[provider] : []; - state.order[provider] = [profile, ...old.filter(x => x !== profile)]; -} -writeJson(stateFile, state); -NODE - -if command -v systemctl >/dev/null 2>&1 && systemctl list-unit-files openclaw-gateway.service >/dev/null 2>&1; then - systemctl restart openclaw-gateway.service - systemctl is-active openclaw-gateway.service -else - echo "Restart OCPlatform now (gateway service not found)." -fi - -``` - -## How to verify - -First check the token without spending model credits: - -```bash -curl -sS -i \ - -H "Authorization: Bearer " \ - http://localhost:8080/v1/token/check -``` - -Then run a provider smoke test from the relevant provider skill. A successful provider response returns HTTP 200 and headers such as `x-gateway-provider` and `x-gateway-account`. - -## Troubleshooting - -- 401 token errors usually mean the full `sp_*` secret was not pasted; the dashboard prefix alone is not enough. -- 403 or "IP not allowed" means this token has an IP allow-list that does not include the machine making the request. -- 429 means the token, user, or upstream account hit a quota or cooldown. -- Anthropic, `kimi-anthropic`, and `glm` must use `authHeader: true` so OCPlatform sends `Authorization: Bearer ...`, not `x-api-key`. -- Native OCPlatform Codex runtime may bypass HTTP base URLs; use the `openai-codex` provider/model if you need gateway-routed Codex traffic. -- Image generation uses the regular `openai` provider for `gpt-image-2`, not `openai-codex`. -- OpenRouter is strict allowlist only; currently configure it for the gateway-listed Gemini models. -- Deepgram is transcription-only and uses `/v1/deepgram/listen`, not an OpenAI chat route. -- xAI uses the Responses API at `/v1/xai/responses`, Grok Voice under `/v1/xai/realtime/client_secrets` / `/v1/xai/tts` / `/v1/xai/stt`, and Grok Imagine media endpoints under `/v1/xai/images/*` / `/v1/xai/videos/*`; non-admin users are denied by default until enabled in Model access or granted. -- Runpod uses OpenAI-compatible chat at `/v1/runpod/chat/completions` with `qwen36-27b` and `qwen36-27b-fast`; non-admin users are denied by default until enabled in Model access or granted. diff --git a/public/skills/anthropic/SKILL.md b/public/skills/anthropic/SKILL.md deleted file mode 100644 index 3621b42..0000000 --- a/public/skills/anthropic/SKILL.md +++ /dev/null @@ -1,149 +0,0 @@ ---- -name: nextbase-anthropic-setup -description: Configure OCPlatform to send Anthropic Messages API traffic through Nextbase using Bearer auth instead of `x-api-key`. ---- - -# Anthropic via Super Proxy - -Anthropic via Nextbase routes `/v1/messages` through the gateway so your agent can use pooled upstream Anthropic accounts without carrying provider keys locally. The gateway validates your `sp_*` token, picks a healthy upstream account, and returns normal Anthropic-compatible responses. - -For Claude Code, use the raw base URL when the client request must reach Anthropic without the gateway's Claude Code/Hermes request rewriting, tool renaming, response reverse mapping, refusal synthesis, or cross-provider fallback: - -```bash -export ANTHROPIC_BASE_URL="http://localhost:8080/v1/anthropic-raw" -export ANTHROPIC_AUTH_TOKEN="sp_" -unset ANTHROPIC_API_KEY -``` - -Claude Code appends `/v1/messages`, producing `POST /v1/anthropic-raw/v1/messages`. Gateway authentication, account selection, model access, limits, usage accounting, and safe auth-header replacement still apply. - -## Prerequisites -- A Nextbase proxy token starting with `sp_` (the user will supply it; if missing, ask) - -## Step 1 — Verify the token works -Run a single curl that hits `http://localhost:8080/v1/token/check` with the user's `sp_*` token. Expect HTTP 200 with `{"ok":true,...}`. If 401, ask the user to confirm the token is correct and not just the `sp_*` prefix. - -```bash -curl -sS -i \ - -H "Authorization: Bearer " \ - http://localhost:8080/v1/token/check -``` - -## Step 2 — Patch OCPlatform config -Default to the JSON merge below because it is less destructive. Merge the provider block into both `~/.openclaw/openclaw.json` under `models.providers.` and `~/.openclaw/agents/main/agent/models.json` under `providers.`. Then merge the auth profile and `lastGood` state. - -### JSON merge option -`~/.openclaw/openclaw.json` provider block: - -```json -{ - "models": { - "providers": { - "anthropic": { - "baseUrl": "http://localhost:8080", - "authHeader": true, - "models": [] - } - } - } -} -``` - -`~/.openclaw/agents/main/agent/models.json` provider block: - -```json -{ - "providers": { - "anthropic": { - "baseUrl": "http://localhost:8080", - "authHeader": true, - "models": [] - } - } -} -``` - -`~/.openclaw/agents/main/agent/auth-profiles.json`: - -```json -{ - "profiles": { - "anthropic:manual": { - "type": "token", - "provider": "anthropic", - "token": "" - } - } -} -``` - -`~/.openclaw/agents/main/agent/auth-state.json`: - -```json -{ - "lastGood": { "anthropic": "anthropic:manual" } -} -``` - -### Automated patch option -Use this only when the user explicitly wants automated patching. It backs up touched files with `.bak-nextbase-`. - -```bash -TOKEN="" BASE_URL="http://localhost:8080" node <<'NODE' -const fs = require('fs'), os = require('os'), path = require('path'); -const token = process.env.TOKEN, baseUrl = process.env.BASE_URL; -const stamp = new Date().toISOString().replace(/[-:T.Z]/g, '').slice(0, 14); -function readJson(file, fallback) { try { const raw = fs.readFileSync(file, 'utf8').trim(); return raw ? JSON.parse(raw) : fallback; } catch { return fallback; } } -function writeJson(file, data) { fs.mkdirSync(path.dirname(file), { recursive: true }); if (fs.existsSync(file)) fs.copyFileSync(file, file + '.bak-nextbase-' + stamp); fs.writeFileSync(file, JSON.stringify(data, null, 2) + '\n'); console.log('patched ' + file); } -const home = os.homedir(); -const provider = { anthropic: { baseUrl, authHeader: true, models: [] } }; -for (const file of [path.join(home, '.openclaw/openclaw.json'), path.join(home, '.openclaw/agents/main/agent/models.json')]) { - const data = readJson(file, {}); - const providers = file.endsWith('openclaw.json') ? (((data.models ||= {}).providers ||= {})) : (data.providers ||= {}); - Object.assign(providers, provider); - writeJson(file, data); -} -const profilesFile = path.join(home, '.openclaw/agents/main/agent/auth-profiles.json'); -const profilesData = readJson(profilesFile, { profiles: {} }); -Object.assign((profilesData.profiles ||= {}), { 'anthropic:manual': { type: 'token', provider: 'anthropic', token } }); -writeJson(profilesFile, profilesData); -const stateFile = path.join(home, '.openclaw/agents/main/agent/auth-state.json'); -const state = readJson(stateFile, {}); state.lastGood ||= {}; Object.assign(state.lastGood, { anthropic: 'anthropic:manual' }); -state.order ||= {}; for (const [p, profile] of Object.entries({ anthropic: 'anthropic:manual' })) { const old = Array.isArray(state.order[p]) ? state.order[p] : []; state.order[p] = [profile, ...old.filter(x => x !== profile)]; } -writeJson(stateFile, state); -NODE -``` - -## Step 3 — Restart OCPlatform -```bash -systemctl restart openclaw-gateway.service || echo "Restart your OCPlatform process manually." -``` - -## Step 4 — Smoke test -Run this provider verify curl exactly as the gateway dashboard uses it: - -```bash -curl -sS -i \ - -H "Authorization: Bearer " \ - -H "Content-Type: application/json" \ - -H "anthropic-version: 2023-06-01" \ - -d '{"model":"claude-sonnet-4-5-20250929","max_tokens":16,"messages":[{"role":"user","content":"pong"}]}' \ - http://localhost:8080/v1/messages -``` - -## What success looks like -- Curl returns HTTP 200. -- Response headers include `x-gateway-provider: anthropic` and `x-gateway-account: Anthropic account label`. - -## Troubleshooting -- 401 token errors usually mean the full `sp_*` secret was not pasted; the dashboard prefix alone is not enough. -- 403 or "IP not allowed" means the token has an IP allow-list that does not include this machine. -- 429 means the token, user, or upstream account hit a quota/cooldown; wait or choose another account/token. -- If OCPlatform sends `x-api-key` to Anthropic-shaped routes, set `authHeader: true` so it sends `Authorization: Bearer ...`. -- For Codex, native `agentRuntime.id: "codex"` bypasses this gateway; use the configured gateway provider/model instead. - -## Provider-specific notes -- Default model is whatever Anthropic model your OCPlatform config selects; the smoke test uses `claude-sonnet-4-5-20250929`. -- Streaming is supported for Messages API streams. -- Cost depends on the upstream Anthropic account routed by the gateway; your `sp_*` token may also have local spending caps. -- Important: keep `authHeader: true`; do not put `apiKey` in `openclaw.json` for Anthropic. diff --git a/public/skills/cerebras/SKILL.md b/public/skills/cerebras/SKILL.md deleted file mode 100644 index e5788b8..0000000 --- a/public/skills/cerebras/SKILL.md +++ /dev/null @@ -1,149 +0,0 @@ ---- -name: nextbase-cerebras-setup -description: Configure OCPlatform to use Cerebras chat through Nextbase. ---- - -# Cerebras via Super Proxy - -Cerebras via Nextbase exposes OpenAI-compatible chat at `/v1/cerebras/chat/completions`. The gateway uses your `sp_*` token for authorization, then routes to an available Cerebras upstream account. Use it for very fast Llama-class chat completions. - -## Prerequisites -- A Nextbase proxy token starting with `sp_` (the user will supply it; if missing, ask) - -## Step 1 — Verify the token works -Run a single curl that hits `http://localhost:8080/v1/token/check` with the user's `sp_*` token. Expect HTTP 200 with `{"ok":true,...}`. If 401, ask the user to confirm the token is correct and not just the `sp_*` prefix. - -```bash -curl -sS -i \ - -H "Authorization: Bearer " \ - http://localhost:8080/v1/token/check -``` - -## Step 2 — Patch OCPlatform config -Default to the JSON merge below because it is less destructive. Merge the provider block into both `~/.openclaw/openclaw.json` under `models.providers.` and `~/.openclaw/agents/main/agent/models.json` under `providers.`. Then merge the auth profile and `lastGood` state. - -### JSON merge option -`~/.openclaw/openclaw.json` provider block: - -```json -{ - "models": { - "providers": { - "cerebras": { - "baseUrl": "http://localhost:8080/v1/cerebras", - "apiKey": "", - "models": [ - { "id": "gpt-oss-120b", "name": "Cerebras GPT OSS 120B" }, - { "id": "zai-glm-4.7", "name": "Cerebras Z.ai GLM 4.7" } - ] - } - } - } -} -``` - -`~/.openclaw/agents/main/agent/models.json` provider block: - -```json -{ - "providers": { - "cerebras": { - "baseUrl": "http://localhost:8080/v1/cerebras", - "apiKey": "", - "models": [ - { "id": "gpt-oss-120b", "name": "Cerebras GPT OSS 120B" }, - { "id": "zai-glm-4.7", "name": "Cerebras Z.ai GLM 4.7" } - ] - } - } -} -``` - -`~/.openclaw/agents/main/agent/auth-profiles.json`: - -```json -{ - "profiles": { - "cerebras:nextbase-gateway": { - "type": "api_key", - "provider": "cerebras", - "key": "" - } - } -} -``` - -`~/.openclaw/agents/main/agent/auth-state.json`: - -```json -{ - "lastGood": { "cerebras": "cerebras:nextbase-gateway" } -} -``` - -### Automated patch option -Use this only when the user explicitly wants automated patching. It backs up touched files with `.bak-nextbase-`. - -```bash -TOKEN="" BASE_URL="http://localhost:8080" node <<'NODE' -const fs = require('fs'), os = require('os'), path = require('path'); -const token = process.env.TOKEN, baseUrl = process.env.BASE_URL; -const stamp = new Date().toISOString().replace(/[-:T.Z]/g, '').slice(0, 14); -function readJson(file, fallback) { try { const raw = fs.readFileSync(file, 'utf8').trim(); return raw ? JSON.parse(raw) : fallback; } catch { return fallback; } } -function writeJson(file, data) { fs.mkdirSync(path.dirname(file), { recursive: true }); if (fs.existsSync(file)) fs.copyFileSync(file, file + '.bak-nextbase-' + stamp); fs.writeFileSync(file, JSON.stringify(data, null, 2) + '\n'); console.log('patched ' + file); } -const home = os.homedir(); -const provider = { cerebras: { baseUrl: baseUrl + '/v1/cerebras', apiKey: token, models: [{ id: 'gpt-oss-120b', name: 'Cerebras GPT OSS 120B' }, { id: 'zai-glm-4.7', name: 'Cerebras Z.ai GLM 4.7' }] } }; -for (const file of [path.join(home, '.openclaw/openclaw.json'), path.join(home, '.openclaw/agents/main/agent/models.json')]) { - const data = readJson(file, {}); - const providers = file.endsWith('openclaw.json') ? (((data.models ||= {}).providers ||= {})) : (data.providers ||= {}); - Object.assign(providers, provider); - writeJson(file, data); -} -const profilesFile = path.join(home, '.openclaw/agents/main/agent/auth-profiles.json'); -const profilesData = readJson(profilesFile, { profiles: {} }); -Object.assign((profilesData.profiles ||= {}), { 'cerebras:nextbase-gateway': { type: 'api_key', provider: 'cerebras', key: token } }); -writeJson(profilesFile, profilesData); -const stateFile = path.join(home, '.openclaw/agents/main/agent/auth-state.json'); -const state = readJson(stateFile, {}); state.lastGood ||= {}; Object.assign(state.lastGood, { cerebras: 'cerebras:nextbase-gateway' }); -state.order ||= {}; for (const [p, profile] of Object.entries({ cerebras: 'cerebras:nextbase-gateway' })) { const old = Array.isArray(state.order[p]) ? state.order[p] : []; state.order[p] = [profile, ...old.filter(x => x !== profile)]; } -writeJson(stateFile, state); -NODE -``` - -## Step 3 — Restart OCPlatform -```bash -systemctl restart openclaw-gateway.service || echo "Restart your OCPlatform process manually." -``` - -## Step 4 — Smoke test -Run this provider verify curl exactly as the gateway dashboard uses it: - -```bash -curl -sS -i \ - -H "Authorization: Bearer " \ - -H "Content-Type: application/json" \ - -d '{"model":"gpt-oss-120b","messages":[{"role":"user","content":"pong"}]}' \ - http://localhost:8080/v1/cerebras/chat/completions -``` - -## What success looks like -- Curl returns HTTP 200. -- Response headers include `x-gateway-provider: cerebras` and `x-gateway-account: Cerebras account label`. - -## Troubleshooting -- 401 token errors usually mean the full `sp_*` secret was not pasted; the dashboard prefix alone is not enough. -- 403 or "IP not allowed" means the token has an IP allow-list that does not include this machine. -- 429 means the token, user, or upstream account hit a quota/cooldown; wait or choose another account/token. -- If OCPlatform sends `x-api-key` to Anthropic-shaped routes, set `authHeader: true` so it sends `Authorization: Bearer ...`. -- For Codex, native `agentRuntime.id: "codex"` bypasses this gateway; use the configured gateway provider/model instead. - -## Provider-specific notes -- Default and fallback model is `gpt-oss-120b` (Production). `zai-glm-4.7` is also available as Preview. -- Streaming chat is supported when the upstream endpoint supports it. -- Cost expectation: free/paid quota depends on the upstream Cerebras account and gateway caps. -- If a requested model is unavailable, the gateway/client config should fall back to `gpt-oss-120b`. - - -## Current Cerebras upstream limits -- Models: `gpt-oss-120b` (Production, 65,536 context) and `zai-glm-4.7` (Preview, 64,000 context). -- Per-model account limits: 5 requests/minute, 150 requests/hour, 2,400 requests/day; 30,000 tokens/minute, 1,000,000 tokens/hour, 1,000,000 tokens/day. diff --git a/public/skills/deepgram/SKILL.md b/public/skills/deepgram/SKILL.md deleted file mode 100644 index 42f14e3..0000000 --- a/public/skills/deepgram/SKILL.md +++ /dev/null @@ -1,75 +0,0 @@ ---- -name: nextbase-deepgram -summary: Configure OCPlatform/OpenClaw to use Deepgram transcription through Super Proxy. ---- - -# Deepgram via Super Proxy - -Deepgram transcription is exposed through Nextbase at `/v1/deepgram/listen`. - -Supported request shapes: - -- JSON URL payloads: `{ "url": "https://example.com/audio.wav" }` -- Raw audio uploads with `Content-Type: audio/*` or `application/octet-stream` - -Policy: - -- Deepgram is strict allowlist only in the gateway. -- Unknown Deepgram model names are blocked before upstream. -- Configure per-user access in the admin Model access tab. - -## Provider config - -```json -{ - "models": { - "providers": { - "deepgram": { - "baseUrl": "http://localhost:8080/v1/deepgram", - "apiKey": "", - "models": [ - { "id": "nova-3", "name": "Deepgram Nova-3" }, - { "id": "nova-2", "name": "Deepgram Nova-2" }, - { "id": "whisper", "name": "Deepgram Whisper Cloud" } - ] - } - } - } -} -``` - -## Auth profile - -```json -{ - "profiles": { - "deepgram:nextbase-gateway": { - "type": "api_key", - "provider": "deepgram", - "key": "" - } - } -} -``` - -## Verify with remote URL - -```bash -curl -sS -i \ - -H "Authorization: Bearer " \ - -H "Content-Type: application/json" \ - -d '{"url":"https://dpgr.am/spacewalk.wav"}' \ - 'http://localhost:8080/v1/deepgram/listen?model=nova-3&smart_format=true' -``` - -## Verify with local audio - -```bash -curl -sS -i \ - -H "Authorization: Bearer " \ - -H "Content-Type: audio/wav" \ - --data-binary @your-audio.wav \ - 'http://localhost:8080/v1/deepgram/listen?model=nova-3&smart_format=true' -``` - -Expected: HTTP 200 with `results.channels[].alternatives[].transcript` and `metadata.duration`. diff --git a/public/skills/fusion/SKILL.md b/public/skills/fusion/SKILL.md deleted file mode 100644 index d376ba7..0000000 --- a/public/skills/fusion/SKILL.md +++ /dev/null @@ -1,213 +0,0 @@ -# Fusion — Multi-Model Deliberation - -The Super Proxy supports **Fusion** — a multi-model deliberation feature that sends your prompt to multiple AI models in parallel, then synthesizes their responses into a single, better answer. - -## Endpoint - -``` -POST /v1/fusion/chat/completions -``` - -Auth: `Authorization: Bearer sp_*` (same token as all other gateway endpoints). - -## Quick Examples - -### Synthesize mode (default) — best answer from multiple models - -```bash -curl -sS -H "Authorization: Bearer $sp_TOKEN" \ - -H "Content-Type: application/json" \ - -d '{ - "model": "fusion/quality", - "messages": [{"role": "user", "content": "Compare Redis vs Memcached for session storage."}], - "stream": true - }' \ - http://localhost:8080/v1/fusion/chat/completions -``` - -### Compare mode — see each model's raw response - -```bash -curl -sS -H "Authorization: Bearer $sp_TOKEN" \ - -H "Content-Type: application/json" \ - -d '{ - "model": "fusion/quality", - "messages": [{"role": "user", "content": "Explain quantum computing."}], - "fusion": {"mode": "compare"} - }' \ - http://localhost:8080/v1/fusion/chat/completions -``` - -### Custom panel (inline, no saved preset) - -```bash -curl -sS -H "Authorization: Bearer $sp_TOKEN" \ - -H "Content-Type: application/json" \ - -d '{ - "model": "fusion/custom", - "messages": [{"role": "user", "content": "Review this design."}], - "fusion": { - "panel": ["anthropic/claude-sonnet-4-5-20250929", "xai/grok-4-fast"], - "synthesizer": "anthropic/claude-sonnet-4-5-20250929" - } - }' \ - http://localhost:8080/v1/fusion/chat/completions -``` - -## Presets - -| Model alias | Panel | Synthesizer | -|---|---|---| -| `fusion` or `fusion/quality` | Claude Opus 4.8, GPT-5.5, Gemini 3.1 Pro | Claude Opus 4.8 | -| `fusion/budget` | Claude Sonnet, GPT-5.4, Gemini 3.5 Flash | Claude Sonnet | -| `fusion/custom` | Requires `fusion.panel` in body | Requires `fusion.synthesizer` in body | -| `fusion/` | User's saved custom preset | User's saved custom preset | - -## Request Body - -Standard OpenAI `/chat/completions` shape with optional `fusion` config: - -```json -{ - "model": "fusion/quality", - "messages": [{"role": "user", "content": "..."}], - "stream": true, - "fusion": { - "mode": "synthesize", - "panel": ["anthropic/claude-sonnet-4-5-20250929", "openai_codex/gpt-5.5"], - "synthesizer": "anthropic/claude-sonnet-4-5-20250929", - "panel_max_tokens": 4096, - "synthesizer_max_tokens": 8192, - "panel_timeout_ms": 120000 - } -} -``` - -When `fusion` body is present, it overrides preset defaults. - -## Response - -Standard `/chat/completions` response with extra `fusion` metadata: - -```json -{ - "id": "fusion-abc123", - "object": "chat.completion", - "model": "fusion/quality", - "choices": [{"index": 0, "message": {"role": "assistant", "content": "..."}, "finish_reason": "stop"}], - "usage": {"prompt_tokens": 2400, "completion_tokens": 1800, "total_tokens": 4200}, - "fusion": { - "mode": "synthesize", - "panel": {"models": ["..."], "succeeded": 3, "failed": 0, "latency_ms": 4200}, - "synthesizer": {"model": "...", "latency_ms": 2200, "succeeded": true}, - "total_latency_ms": 6400 - } -} -``` - -## Model Discovery - -Fusion models appear in `GET /v1/models` so clients like Cursor can list them: - -```bash -curl -sS -H "Authorization: Bearer $sp_TOKEN" \ - http://localhost:8080/v1/models -``` - -## OCPlatform Setup - -Fusion uses the `openai-completions` API adapter. The `baseUrl` must end with `/v1/fusion` — the adapter appends `/chat/completions` to form the full endpoint URL. - -**Important:** The model IDs contain a slash (`fusion/quality`), so the full OpenClaw model reference is `fusion/fusion/quality` (provider name / model ID). This is how OpenClaw namespaces models. - -### Provider block — merge into `openclaw.json` and `models.json` - -```json -{ - "models": { - "providers": { - "fusion": { - "baseUrl": "http://localhost:8080/v1/fusion", - "apiKey": "", - "api": "openai-completions", - "models": [ - { "id": "fusion/max", "name": "Fusion Max", "reasoning": true, "input": ["text"], "contextWindow": 200000, "maxTokens": 8192 }, - { "id": "fusion/quality", "name": "Fusion Quality", "reasoning": true, "input": ["text"], "contextWindow": 200000, "maxTokens": 8192 }, - { "id": "fusion/budget", "name": "Fusion Budget", "reasoning": true, "input": ["text"], "contextWindow": 200000, "maxTokens": 8192 } - ] - } - } - } -} -``` - -### Auth profile — merge into `auth-profiles.json` - -```json -{ - "profiles": { - "fusion:nextbase-gateway": { - "type": "api_key", - "provider": "fusion", - "key": "" - } - } -} -``` - -### Agent defaults — merge into `openclaw.json` - -**Critical:** Without this, `/models` won't list fusion models and `/model` won't accept them. - -```json -{ - "agents": { - "defaults": { - "models": { - "fusion/fusion/max": {}, - "fusion/fusion/quality": {}, - "fusion/fusion/budget": {} - } - } - } -} -``` - -### Verify - -```bash -# Check token -curl -sS -H "Authorization: Bearer " \ - http://localhost:8080/v1/token/check - -# Test fusion directly -curl -sS -H "Authorization: Bearer " \ - -H "Content-Type: application/json" \ - -d '{"model":"fusion/budget","messages":[{"role":"user","content":"pong"}]}' \ - http://localhost:8080/v1/fusion/chat/completions -``` - -Then in OpenClaw: `/model fusion/fusion/quality` - -### Key notes - -- `api` must be `"openai-completions"` (not `"openai-chat-completions"` — that doesn't exist) -- `baseUrl` must end with `/v1/fusion` — the adapter appends `/chat/completions` automatically -- Model refs in OpenClaw are `fusion/fusion/max`, `fusion/fusion/quality`, `fusion/fusion/budget` (double `fusion/` because the provider is named `fusion` and the model IDs start with `fusion/`) -- No `authHeader: true` needed — the apiKey on the provider block handles auth -- Add all three models to `agents.defaults.models` or they won't appear in `/models` - -## Provider Prefixes - -Panel and synthesizer models use `provider/model` format: - -| Prefix | Provider | -|---|---| -| `anthropic/` | Anthropic (Claude) | -| `openai_codex/` | OpenAI/Codex (GPT) | -| `gemini/` | Google Gemini | -| `groq/` | Groq | -| `cerebras/` | Cerebras | -| `kimi/` | Kimi/Moonshot | -| `xai/` | xAI (Grok) | -| `openrouter/` | OpenRouter | diff --git a/public/skills/gemini/LIVE.md b/public/skills/gemini/LIVE.md deleted file mode 100644 index dc77d8d..0000000 --- a/public/skills/gemini/LIVE.md +++ /dev/null @@ -1,85 +0,0 @@ -# Gemini Live (realtime) via Super Proxy - -Gemini **Live** is a bidirectional, low-latency **WebSocket** session (audio in / audio -out), not a request/response call. It does **not** ride `/v1/gemini/chat/completions`. -Nextbase exposes it through a dedicated relay so clients never see the upstream key. - -> **Audio-first:** all Live models require the `AUDIO` response modality. `TEXT`-only -> response config is rejected by upstream. - -## Models - -| Model id | Use | -|---|---| -| `gemini-2.5-flash-native-audio-preview-12-2025` | Native-audio realtime voice dialog (default) | -| `gemini-3.1-flash-live-preview` | Newer Flash Live realtime model | -| `gemini-3.5-live-translate-preview` | Realtime speech-to-speech translation | - -Aliases also registered: `gemini-2.5-flash-native-audio-latest`, -`gemini-2.5-flash-native-audio-preview-09-2025`. - -Verified live 2026-06-24: all three reach `setupComplete` and return real audio -(native-audio 28.8 KB; flash-live 42 KB + transcription; translate 972 KB, -input transcribed). - -## Two ways to connect - -### 1. Relay WebSocket (works today) — `wss:///v1/gemini/realtime` - -The gateway holds the Gemini key and pins one pooled account per session. Browsers -can't set headers on a WebSocket, so pass the gateway token as a query param -(`access_token` / `token`); server-side clients may use `Authorization: Bearer`. - -``` -wss:///v1/gemini/realtime?model=gemini-2.5-flash-native-audio-preview-12-2025&access_token=sp_xxx -``` - -Then speak the native Live protocol directly — the relay forwards frames verbatim -in both directions: - -1. First client message: `{ "setup": { "model": "models/", "generationConfig": { "responseModalities": ["AUDIO"] }, ... } }` -2. Server replies `{ "setupComplete": {} }`. -3. Stream input: `{ "realtimeInput": { "audio": { "data": "", "mimeType": "audio/pcm;rate=16000" } } }`, then `{ "realtimeInput": { "audioStreamEnd": true } }`. -4. Receive `serverContent.modelTurn.parts[].inlineData` audio chunks; turn ends on `turnComplete`. - -Because setup is forwarded unchanged, any setup feature works automatically once -the upstream key tier supports it (see gate below). - -### 2. Ephemeral client secret — `POST /v1/gemini/realtime/client_secrets` - -Mints a short-lived Google token so a browser/OCPlatform client can connect directly -to Google's Live WS without the relay. Auth with your `sp_*` token; body optional: - -```json -{ "model": "gemini-3.1-flash-live-preview", "uses": 1, "expire_minutes": 30 } -``` - -Returns the upstream `auth_tokens` response passed through unchanged. - -> **Upstream gate (2026-06-24):** ephemeral `auth_tokens` minting and Live Translate -> `translationConfig.targetLanguageCode` both currently return `API key not valid` -> on our free key tier — they require allowlisting. The **relay path works today**; -> Live Translate works to its **default target (English)** now. When keys are -> allowlisted, target-language selection and ephemeral secrets work with **zero -> code change** (setup is forwarded verbatim). - -## Live Translate target language - -Place inside `generationConfig` (top-level placement is rejected): - -```json -{ "setup": { "model": "models/gemini-3.5-live-translate-preview", - "generationConfig": { "responseModalities": ["AUDIO"], - "translationConfig": { "targetLanguageCode": "es" } }, - "outputAudioTranscription": {} } } -``` - -(BCP-47 code; default target is `en`. Gated until keys are allowlisted — see above.) - -## Limits / notes - -- Live runs in its own pool family (`live`) with a conservative daily cap so it - can't starve embeddings/chat/video budgets. -- Sessions are internal-only and unmetered for cost; a zero-cost `usage_event` is - recorded per session for observability. -- Max WS frame: 8 MB (covers chunked PCM turns). diff --git a/public/skills/gemini/SKILL.md b/public/skills/gemini/SKILL.md deleted file mode 100644 index 75ad199..0000000 --- a/public/skills/gemini/SKILL.md +++ /dev/null @@ -1,151 +0,0 @@ ---- -name: nextbase-gemini -description: Configure an agent client to use the native Gemini free-tier pool (embeddings, chat, TTS) through Super Proxy. ---- - -# Gemini via Super Proxy - -Native Gemini is exposed through Nextbase under `/v1/gemini/*` as **OpenAI-compatible** routes, backed by a pool of free-tier Gemini API keys (each a separate Google Cloud project). The gateway rotates across keys by lowest daily usage, proactively skips keys near their per-model daily cap, and fails over on `429` — so the tiny per-key free-tier limits add up across the pool. - -> This is **distinct** from the `openrouter` provider's Gemini models. Those route OpenAI-style chat to OpenRouter's paid Gemini. This native pool is the free-tier one, and its main job is **pooled embeddings for memory search**. - -## Routes - -- `POST /v1/gemini/embeddings` — OpenAI-shape embeddings (primary use: memorySearch) -- `POST /v1/gemini/chat/completions` — OpenAI-shape chat (non-streaming) -- `POST /v1/gemini/tts` — text-to-speech, returns a WAV (24 kHz / 16-bit / mono) -- `GET /v1/gemini/realtime` (WebSocket) — Gemini **Live** realtime audio relay -- `POST /v1/gemini/realtime/client_secrets` — mint an ephemeral Live token - -Gemini **Live** (realtime bidirectional audio) is a WebSocket session, not a chat -call. See **[LIVE.md](./LIVE.md)** for models, the relay/ephemeral connection -flows, and the current upstream allowlist gate. - -## Models - -- Embeddings: `gemini-embedding-2` (stable, 3072-dim), `gemini-embedding-2-preview`, `gemini-embedding-001` -- Chat: `gemini-3.1-flash-lite` (and other flash-lite variants) -- Video understanding: `gemini-3.5-flash` (recommended), `gemini-2.5-flash` — send a video to the chat route. See **[VIDEO-UNDERSTANDING.md](./VIDEO-UNDERSTANDING.md)** for endpoint, request shape, size/length limits, and how to cut long videos. -- TTS: `gemini-2.5-flash-preview-tts` (default voice `Kore`) - -Only known Gemini model IDs are allowed; unknown models return `400`. - -## Set up embeddings in your OCPlatform (step by step) - -This wires OCPlatform's **built-in `memorySearch`** (semantic search over your workspace files) to the gateway's pooled Gemini embeddings. You don't need any custom memory system — `memorySearch` ships with OCPlatform; you just point it at us. - -### 1. Get a gateway token - -Ask your gateway admin for an `sp_*` API token (or mint one in the console under **Tokens**). This single token is all your OCPlatform needs — it never sees the upstream Gemini keys. - -### 2. Open your OCPlatform config - -Edit `~/.openclaw/openclaw.json` (the file behind your OCPlatform install). Back it up first: - -```bash -cp ~/.openclaw/openclaw.json ~/.openclaw/openclaw.json.bak -``` - -### 3. Add the `memorySearch` block - -Put it under `agents.defaults` so it applies to every agent (or under a specific agent if you prefer). Merge this into your existing JSON — don't overwrite the whole file: - -```json -{ - "agents": { - "defaults": { - "memorySearch": { - "enabled": true, - "provider": "openai", - "model": "gemini-embedding-2", - "extraPaths": ["docs/", "reports/"], - "remote": { - "baseUrl": "http://localhost:8080/v1/gemini", - "apiKey": "sp_YOUR_TOKEN_HERE" - } - } - } - } -} -``` - -What each field does: -- `provider: "openai"` — OCPlatform talks OpenAI-shape embeddings; the gateway's `/v1/gemini` route speaks that shape, so no custom provider code is needed. -- `model: "gemini-embedding-2"` — the embedding model (3072-dim). Keep this fixed; changing the model id forces a full re-embed. -- `extraPaths` — optional extra folders (relative to your workspace) to index beyond the defaults. Drop it to use defaults only. -- `remote.baseUrl` — must end in `/v1/gemini` (OCPlatform appends `/embeddings`). -- `remote.apiKey` — your `sp_*` token. - -### 4. Restart OpenClaw - -```bash -# system-service install -systemctl restart openclaw-gateway.service -# or, if you run it directly, restart your openclaw process -``` - -### 5. Build the index - -The index builds automatically the first time memorySearch runs. To build it now and watch progress: - -```bash -openclaw memory index --force -openclaw memory status # shows Provider openai / Model gemini-embedding-2 / Indexed N files -``` - -That's it — `memory_search` now returns semantic hits over your files, embedded through the pooled gateway. Re-running `openclaw memory index` after adding files only embeds the new/changed chunks (progress is cached). - -> The model stays a Gemini embedding model and only the route changes, so an index already built on `gemini-embedding-2` stays vector-compatible — no re-embed when the model id matches. - -### Heads-up on free-tier throughput - -Embeddings free-tier is **per key per day** (Pacific-midnight reset). A large first-time index can exhaust the daily pool and pause partway — it resumes automatically next day, or your admin can add more pooled keys. Small/incremental indexes are fine. - -## Optional — chat provider snippet - -```json -{ - "models": { - "providers": { - "gemini-nextbase": { - "baseUrl": "http://localhost:8080/v1/gemini", - "apiKey": "", - "models": [ - { "id": "gemini-3.1-flash-lite", "name": "Gemini 3.1 Flash Lite" } - ] - } - } - } -} -``` - -## Verify - -Embeddings: - -```bash -curl -sS -i \ - -H "Authorization: Bearer " \ - -H "Content-Type: application/json" \ - -d '{"model":"gemini-embedding-2","input":"pong"}' \ - http://localhost:8080/v1/gemini/embeddings -``` - -Returns `200` with `{"object":"list","data":[{"embedding":[...3072 floats]}],"usage":{...}}` and an `x-gateway-account` header naming the pool key that served it. - -Chat: - -```bash -curl -sS -i \ - -H "Authorization: Bearer " \ - -H "Content-Type: application/json" \ - -d '{"model":"gemini-3.1-flash-lite","messages":[{"role":"user","content":"pong"}]}' \ - http://localhost:8080/v1/gemini/chat/completions -``` - -## Notes - -- Auth to the gateway is the `sp_*` Bearer token (the gateway holds the upstream Gemini keys; clients never see them). -- Cost is logged as `$0` (free tier) but every call records a usage row for attribution. -- Free-tier caps are **per key per model per day** (Pacific midnight reset). Pooling N keys multiplies the daily ceiling ~N×. -- All-keys-exhausted returns a clean `429` with a `retry-after` to the next Pacific midnight. diff --git a/public/skills/gemini/VIDEO-UNDERSTANDING.md b/public/skills/gemini/VIDEO-UNDERSTANDING.md deleted file mode 100644 index 0cf4d4e..0000000 --- a/public/skills/gemini/VIDEO-UNDERSTANDING.md +++ /dev/null @@ -1,295 +0,0 @@ ---- -name: nextbase-gemini-video -description: Send a video to the Super Proxy and get back an understanding/description using Gemini's video-capable models, including size/length limits and how to cut long videos. ---- - -# Gemini Video Understanding via Super Proxy - -Send a video, get back text understanding (description, transcription of on-screen action, Q&A about the footage). This runs on Gemini's video-capable models through the gateway's pooled free-tier keys — your client only ever holds one `sp_*` token. - ---- - -## TL;DR - -- **Endpoint:** `POST http://localhost:8080/v1/gemini/chat/completions` -- **Auth:** `Authorization: Bearer sp_...` (your gateway token) -- **Shape:** OpenAI chat-completions, with a `video_url` content part -- **Models:** `gemini-3.5-flash` (recommended) or `gemini-2.5-flash` -- **Inline video limit:** ~**20 MB** raw file (request body cap is 30 MB). Bigger → **upload via the File API** (`POST /v1/gemini/files`, up to ~2 GB — see Option C), use a hosted URL, or cut/compress (recipe below). -- **Free-tier budget:** ~**20 video requests/day per key**, ~**300/day pooled**. Best-effort, not for high volume. -- **Routes:** chat = `/v1/gemini/chat/completions`; large uploads = `POST/GET/DELETE /v1/gemini/files`. - ---- - -## 1. Get a token - -Ask your gateway admin for an `sp_*` token, or mint one in the console under **Tokens**. The token never sees the upstream Gemini keys. - ---- - -## 2. Which endpoint + model - -| | | -|---|---| -| **URL** | `http://localhost:8080/v1/gemini/chat/completions` | -| **Method** | `POST` (non-streaming) | -| **Header** | `Authorization: Bearer sp_...` | -| **Header** | `Content-Type: application/json` | -| **Model** | `gemini-3.5-flash` (faster, ~3× cheaper, recommended) or `gemini-2.5-flash` (also counts an audio track) | - -> Only these two models do video. The flash-**lite** chat models (`gemini-3.1-flash-lite`, `gemini-2.5-flash-lite`) are text-only and will reject video. - ---- - -## 3. How to send a video - -You can attach the video two ways. Use a `video_url` content part inside an OpenAI-style message. - -### Option A — Inline base64 (files up to ~20 MB) - -Encode the file as a `data:` URI and embed it. No upload step, no File API. - -```bash -TOKEN="sp_your_token_here" -VIDEO="clip.mp4" - -# Build the request body (base64 the video into a data URI) -python3 - "$VIDEO" <<'PY' > /tmp/body.json -import sys, json, base64 -f = sys.argv[1] -b64 = base64.b64encode(open(f, "rb").read()).decode() -body = { - "model": "gemini-3.5-flash", - "messages": [{ - "role": "user", - "content": [ - {"type": "text", "text": "Describe what happens in this video. Who appears and what is the setting?"}, - {"type": "video_url", "video_url": {"url": f"data:video/mp4;base64,{b64}"}} - ] - }] -} -json.dump(body, open("/tmp/body.json", "w")) -PY - -curl -s "http://localhost:8080/v1/gemini/chat/completions" \ - -H "Authorization: Bearer $TOKEN" \ - -H "Content-Type: application/json" \ - --data-binary @/tmp/body.json | python3 -m json.tool -``` - -Response is normal OpenAI chat shape: - -```json -{ - "model": "gemini-3.5-flash", - "choices": [{ "message": { "role": "assistant", "content": "Set in a charity gala ..." } }], - "usage": { "prompt_tokens": 2746, "completion_tokens": 36, "total_tokens": 3147 } -} -``` - -### Option B — Hosted URL (for bigger files or a public YouTube link) - -If the video is reachable over `http(s)` (a public URL or a YouTube link), pass the URL directly — Gemini fetches it, so you skip the body-size limit: - -```json -{ - "model": "gemini-3.5-flash", - "messages": [{ - "role": "user", - "content": [ - {"type": "text", "text": "Summarize this video."}, - {"type": "video_url", "video_url": {"url": "https://www.youtube.com/watch?v=XXXXXXXXXXX"}} - ] - }] -} -``` - -> The URL must be publicly fetchable by Google. Private/expiring/signed URLs that need your auth headers will not work — use inline base64 (Option A) or upload via the File API (Option C) for those. - -### Option C — Upload large/private files (File API, up to ~2 GB) - -For videos too big for inline (~20 MB+) that you **don't** want to host publicly, upload them to the gateway's File API. The file is stored privately for **48 hours** (then auto-deleted), and you reference it by the returned `file_uri`. Works on the free tier (no billing needed). - -**Step 1 — upload the raw file bytes:** - -```bash -TOKEN="sp_your_token_here" -BASE="http://localhost:8080" - -curl -s -X POST "$BASE/v1/gemini/files?display_name=myvideo" \ - -H "Authorization: Bearer $TOKEN" \ - -H "Content-Type: video/mp4" \ - --data-binary @big-video.mp4 -``` - -Response: - -```json -{ - "name": "files/oq7evufk8fmq", - "file_uri": "https://generativelanguage.googleapis.com/v1beta/files/oq7evufk8fmq?sp_acct=48", - "mime_type": "video/mp4", - "size_bytes": 9054010, - "state": "PROCESSING" -} -``` - -> **Keep the `file_uri` exactly as returned** — it carries a tag (`?sp_acct=…`) the gateway uses to route your follow-up request to the correct key. Don't strip it. - -**Step 2 — (optional) wait until it's `ACTIVE`:** - -Large videos may take a few seconds to process. Poll the status (URL-encode the `name` + tag): - -```bash -curl -s "$BASE/v1/gemini/files/oq7evufk8fmq%3Fsp_acct%3D48" \ - -H "Authorization: Bearer $TOKEN" -# -> { "state": "ACTIVE", ... } (wait for ACTIVE before step 3) -``` - -**Step 3 — ask about it** (pass the `file_uri` as a `video_url`): - -```json -{ - "model": "gemini-3.5-flash", - "messages": [{ - "role": "user", - "content": [ - {"type": "text", "text": "Summarize this video."}, - {"type": "video_url", "video_url": {"url": "https://generativelanguage.googleapis.com/v1beta/files/oq7evufk8fmq?sp_acct=48"}} - ] - }] -} -``` - -**Step 4 — (optional) delete early** (files auto-expire after 48h anyway): - -```bash -curl -s -X DELETE "$BASE/v1/gemini/files/oq7evufk8fmq%3Fsp_acct%3D48" \ - -H "Authorization: Bearer $TOKEN" -``` - -> **Key-affinity (why the tag matters):** an uploaded file is private to the one pooled key that uploaded it, so the gateway *must* run your follow-up request on that same key — that's what the `sp_acct` tag encodes. Side effect: a File-API request **can't fail over** to another key. If that key is rate-limited/exhausted when you ask, the request fails (re-upload to get a fresh key). For most uploads this is invisible. - ---- - -## 4. Limits — read this before sending - -### Request size (the practical wall for inline) -- **Inline base64:** the gateway accepts request bodies up to **30 MB**. Base64 inflates a file ~33%, so that's roughly a **20 MB raw video**. This matches Gemini's own ~20 MB inline cap. -- **Over ~20 MB:** **upload via the File API** (Option C, up to ~2 GB), use Option B (hosted URL), or cut/compress first (Section 5). The File API is the cleanest path for big private videos — no public hosting, free tier, 48h retention. - -### Video length (how long a clip Gemini can reason about) -The duration ceiling is huge — body size limits you first for inline. For reference (model context dependent): - -| Resolution mode | Max duration (large-context models) | -|---|---| -| Default media resolution | up to ~**2 hours** | -| Low media resolution | up to ~**6 hours** | -| Gemini 2.5 Pro (with audio) | ~45 min | - -In practice through this gateway, **file size (~20 MB inline) caps you well before duration does.** A clip that's short enough to fit 20 MB is always within the duration limit. - -### Rough "how long fits in 20 MB inline" -Depends entirely on bitrate. As a guide: - -| Encoding | Approx duration in 20 MB | -|---|---| -| 1080p, normal bitrate (~8 Mbps) | ~20 sec | -| 720p (~2.5 Mbps) | ~60 sec | -| 480p, compressed (~1 Mbps) | ~2.5 min | -| Low-res analysis encode (~0.5 Mbps) | ~5 min | - -So **to send a longer clip inline, drop the resolution/bitrate** (Section 5) — for "what's happening in this video" you rarely need 1080p. - -### Tokens & cost signal -- ~**300 tokens per second of video** at default resolution (258/frame at 1 FPS + ~32/sec audio); ~100 tokens/sec at low resolution. -- A 30s clip ≈ 3–10K tokens. Per-request that's well within limits (250K tokens/min/key). The real wall is the **daily request count**, not tokens. - -### Throughput / quota (free-tier pool) -- ~**20 video requests per day, per key.** Pooled across the current keys that's ~**300 requests/day total**, shared by everyone. -- This is **best-effort** capacity for demos and light use. If you hit `429`/quota errors, the pool's daily video budget is spent — try again after Pacific midnight, or ask the admin to add billing-enabled keys for real volume. -- Limits reset at **midnight US Pacific time**. - ---- - -## 5. Sending a longer video — cut and/or compress it - -If your video is longer than ~20 MB will hold (or you want the whole thing), do **one** of these: - -### 5a. Compress to fit (keep the whole clip, lower the quality) -Best when you want the full video and don't need high resolution. Scale down + lower bitrate so the whole clip lands under ~18 MB: - -```bash -# Re-encode to 480p, ~0.8 Mbps video + low audio — good enough for "what's happening" -ffmpeg -i input.mp4 -vf "scale=-2:480" -b:v 800k -b:a 64k -movflags +faststart small.mp4 -ls -lh small.mp4 # aim for < 18 MB -``` - -If it's still too big, drop further: `scale=-2:360` and `-b:v 500k`. - -### 5b. Cut into chunks and send each one (keep quality, split by time) -Best when you need the detail and the video is long. Split into fixed-length segments, then send each segment as a separate request and stitch the answers. - -```bash -# Split into 60-second segments: out_000.mp4, out_001.mp4, ... -ffmpeg -i input.mp4 -c copy -map 0 -f segment -segment_time 60 -reset_timestamps 1 out_%03d.mp4 - -# Check each chunk's size; if a chunk is still > ~18 MB, compress it (5a) or use a shorter -segment_time -ls -lh out_*.mp4 -``` - -Then loop the chunks through the endpoint: - -```bash -TOKEN="sp_your_token_here" -for f in out_*.mp4; do - python3 - "$f" > /tmp/body.json <<'PY' -import sys, json, base64 -f = sys.argv[1] -b64 = base64.b64encode(open(f, "rb").read()).decode() -json.dump({ - "model": "gemini-3.5-flash", - "messages": [{"role":"user","content":[ - {"type":"text","text": f"This is segment {f} of a longer video. Describe what happens in this segment."}, - {"type":"video_url","video_url":{"url": f"data:video/mp4;base64,{b64}"}} - ]}] -}, open("/tmp/body.json","w")) -PY - echo "=== $f ===" - curl -s "http://localhost:8080/v1/gemini/chat/completions" \ - -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \ - --data-binary @/tmp/body.json \ - | python3 -c "import sys,json;print(json.load(sys.stdin)['choices'][0]['message']['content'])" - sleep 2 # be gentle on the pooled free-tier quota -done -``` - -> **Watch your daily budget when chunking.** Each chunk is one request against the ~20/key (~300 pooled) daily video limit. A 10-minute video at 60s chunks = 10 requests. For very long videos, prefer compression (5a) or a hosted URL (Option B) over many chunks. - -### 5c. Combine: cut to the part you care about -If you only need a section, trim first — fastest and cheapest: - -```bash -# Take 90 seconds starting at 02:00 -ffmpeg -ss 00:02:00 -i input.mp4 -t 90 -c copy clip.mp4 -``` - ---- - -## 6. Quick checklist - -1. Have an `sp_*` token. -2. Video ≤ ~20 MB? → send inline (Option A). Bigger? → **upload via the File API** (Option C, up to ~2 GB), use a hosted URL (Option B), or compress/cut (Section 5). -3. POST to `/v1/gemini/chat/completions` with `model: gemini-3.5-flash` and a `video_url` part (an inline data URI, a hosted URL, or a File API `file_uri`). -4. Got a `429`/quota error? The pooled daily video budget is spent — retry after Pacific midnight or ask the admin for billing-enabled keys. - ---- - -## Notes / gotchas - -- **Non-streaming only.** No SSE on this route. -- **`gemini-3.5-flash` is the recommended default** for video: faster and ~3× fewer tokens than `gemini-2.5-flash`, equally accurate in testing. Use `gemini-2.5-flash` if you specifically want audio-track analysis counted. -- **Multimodal mixing works:** you can also send `image_url`, `audio_url`, and `input_audio` parts the same way. Data URIs become inline data; `http(s)` URLs are fetched by Gemini. -- **One video per request** is the sweet spot. Multiple large inline videos will blow the 30 MB body limit. -- **File API uploads (Option C):** keep the returned `file_uri` verbatim (it carries the `sp_acct` routing tag); files live 48h then auto-delete; a File-API request can't fail over to another pooled key (re-upload if you hit a quota error on that key). -- This is the **native free-tier Gemini pool**, distinct from the `openrouter` Gemini models. diff --git a/public/skills/glm/SKILL.md b/public/skills/glm/SKILL.md deleted file mode 100644 index 39b8783..0000000 --- a/public/skills/glm/SKILL.md +++ /dev/null @@ -1,98 +0,0 @@ ---- -name: nextbase-glm-setup -description: Configure Claude Code / OpenClaw for z.ai GLM Coding Plan through Nextbase via a transparent Anthropic Messages passthrough. ---- - -# GLM (z.ai) via Super Proxy - -GLM via Nextbase exposes a transparent Anthropic-compatible Messages endpoint at `/v1/glm/v1/messages` (canonical, what Anthropic SDKs hit when `ANTHROPIC_BASE_URL=.../v1/glm`) — `/v1/glm/messages` also works as a short alias for direct callers. It proxies to z.ai's GLM Coding Plan Anthropic-compatible endpoint (`https://api.z.ai/api/anthropic/v1/messages`) so a Claude-Code / OpenClaw Anthropic request "just works" — the body is forwarded verbatim and the pooled subscription key is injected as `x-api-key`. Use this to run Claude Code against GLM models routed through Nextbase with a single `sp_*` token. - -## Models -- `glm-5.2` (default), `glm-5.1`, `glm-5`, `glm-5-turbo`, `glm-4.7`, `glm-4.6` -- There is no `[1m]` / `-1m` long-context variant on this plan (those 400). - -## Prerequisites -- A Nextbase proxy token starting with `sp_` (the user will supply it; if missing, ask). - -## Step 1 — Verify the token works -```bash -curl -sS -i \ - -H "Authorization: Bearer " \ - http://localhost:8080/v1/token/check -``` -Expect HTTP 200 with `{"ok":true,...}`. A 401 means the token is wrong or only the dashboard prefix was pasted. - -## Step 2 — Configure Claude Code -GLM is Anthropic-format, so point Claude Code's Anthropic base URL at the gateway's `/v1/glm` route. The gateway accepts the `sp_*` token via either `x-api-key` (Claude Code's default) or `Authorization: Bearer`. - -```bash -export ANTHROPIC_BASE_URL="http://localhost:8080/v1/glm" -export ANTHROPIC_AUTH_TOKEN="" # also accepted as ANTHROPIC_API_KEY -export ANTHROPIC_MODEL="glm-5.2" -claude -``` - -`settings.json` equivalent (`~/.claude/settings.json`): -```json -{ - "env": { - "ANTHROPIC_BASE_URL": "http://localhost:8080/v1/glm", - "ANTHROPIC_AUTH_TOKEN": "", - "ANTHROPIC_MODEL": "glm-5.2" - } -} -``` - -## Step 2b — OpenClaw config (optional) -Merge an Anthropic-shaped provider block into both `~/.openclaw/openclaw.json` under `models.providers.glm` and `~/.openclaw/agents/main/agent/models.json` under `providers.glm`. Because the route is Anthropic-format, use `authHeader: true` so OCPlatform sends Bearer auth. - -```json -{ - "models": { - "providers": { - "glm": { - "baseUrl": "http://localhost:8080/v1/glm", - "authHeader": true, - "apiKey": "", - "models": [ - { "id": "glm-5.2", "name": "GLM 5.2" }, - { "id": "glm-5.1", "name": "GLM 5.1" }, - { "id": "glm-5", "name": "GLM 5" }, - { "id": "glm-5-turbo", "name": "GLM 5 Turbo" }, - { "id": "glm-4.7", "name": "GLM 4.7" }, - { "id": "glm-4.6", "name": "GLM 4.6" } - ] - } - } - } -} -``` - -## Step 3 — Smoke test -```bash -curl -sS -i \ - -H "x-api-key: " \ - -H "anthropic-version: 2023-06-01" \ - -H "Content-Type: application/json" \ - -d '{"model":"glm-5.2","max_tokens":64,"messages":[{"role":"user","content":"pong"}]}' \ - http://localhost:8080/v1/glm/messages -``` - -## What success looks like -- Curl returns HTTP 200 with a standard Anthropic Messages JSON body (`type:"message"`, `content`, `usage`). -- Response headers include `x-gateway-provider: glm` and `x-gateway-account: `. - -## How it disguises as Claude Code (transparent passthrough) -- The request body is forwarded **verbatim** to `https://api.z.ai/api/anthropic/v1/messages` — `system`, `tools`, `anthropic-beta` betas, and `stream` are untouched. -- Your client's own `user-agent`, `anthropic-beta`, and `x-stainless-*` headers pass through unchanged, so the upstream request keeps the exact Claude-Code fingerprint your client produced. -- Only the auth + hop-by-hop headers are rewritten: the gateway strips your `sp_*` token and injects the pooled subscription key as `x-api-key`, defaulting `anthropic-version: 2023-06-01` if absent. - -## Troubleshooting -- 401 token errors usually mean the full `sp_*` secret was not pasted; the dashboard prefix alone is not enough. -- 429 means the token, user, or upstream GLM account hit a quota/cooldown; the gateway auto-rotates accounts and adds `retry-after`. -- A `400` mentioning an unknown model usually means a non-plan id (e.g. a `-1m` variant); use one of the six models above. - -## Provider-specific notes -- Default model is `glm-5.2`; unknown/unsupported ids fall back to it and set `x-gateway-glm-fallback`. -- Streaming (SSE) is supported and passed through transparently. -- Cost: the GLM Coding Plan is flat-rate, so usage is recorded as zero-cost usage_events (token counts are still captured for visibility). diff --git a/public/skills/groq/SKILL.md b/public/skills/groq/SKILL.md deleted file mode 100644 index f443ba5..0000000 --- a/public/skills/groq/SKILL.md +++ /dev/null @@ -1,142 +0,0 @@ ---- -name: nextbase-groq-setup -description: Configure OCPlatform to use Groq chat and Whisper STT routes through Nextbase. ---- - -# Groq via Super Proxy - -Groq via Nextbase exposes OpenAI-compatible chat under `/v1/groq/chat/completions` and Whisper-compatible speech routes under `/v1/groq/audio/transcriptions` and `/v1/groq/audio/translations`. The gateway validates your `sp_*` token and forwards to healthy Groq upstream accounts. Use this for fast chat models and low-latency STT. - -## Prerequisites -- A Nextbase proxy token starting with `sp_` (the user will supply it; if missing, ask) - -## Step 1 — Verify the token works -Run a single curl that hits `http://localhost:8080/v1/token/check` with the user's `sp_*` token. Expect HTTP 200 with `{"ok":true,...}`. If 401, ask the user to confirm the token is correct and not just the `sp_*` prefix. - -```bash -curl -sS -i \ - -H "Authorization: Bearer " \ - http://localhost:8080/v1/token/check -``` - -## Step 2 — Patch OCPlatform config -Default to the JSON merge below because it is less destructive. Merge the provider block into both `~/.openclaw/openclaw.json` under `models.providers.` and `~/.openclaw/agents/main/agent/models.json` under `providers.`. Then merge the auth profile and `lastGood` state. - -### JSON merge option -`~/.openclaw/openclaw.json` provider block: - -```json -{ - "models": { - "providers": { - "groq": { - "baseUrl": "http://localhost:8080/v1/groq", - "apiKey": "", - "models": [ - { "id": "openai/gpt-oss-120b", "name": "Groq GPT OSS 120B" } - ] - } - } - } -} -``` - -`~/.openclaw/agents/main/agent/models.json` provider block: - -```json -{ - "providers": { - "groq": { - "baseUrl": "http://localhost:8080/v1/groq", - "apiKey": "", - "models": [ - { "id": "openai/gpt-oss-120b", "name": "Groq GPT OSS 120B" } - ] - } - } -} -``` - -`~/.openclaw/agents/main/agent/auth-profiles.json`: - -```json -{ - "profiles": { - "groq:nextbase-gateway": { - "type": "api_key", - "provider": "groq", - "key": "" - } - } -} -``` - -`~/.openclaw/agents/main/agent/auth-state.json`: - -```json -{ - "lastGood": { "groq": "groq:nextbase-gateway" } -} -``` - -### Automated patch option -Use this only when the user explicitly wants automated patching. It backs up touched files with `.bak-nextbase-`. - -```bash -TOKEN="" BASE_URL="http://localhost:8080" node <<'NODE' -const fs = require('fs'), os = require('os'), path = require('path'); -const token = process.env.TOKEN, baseUrl = process.env.BASE_URL; -const stamp = new Date().toISOString().replace(/[-:T.Z]/g, '').slice(0, 14); -function readJson(file, fallback) { try { const raw = fs.readFileSync(file, 'utf8').trim(); return raw ? JSON.parse(raw) : fallback; } catch { return fallback; } } -function writeJson(file, data) { fs.mkdirSync(path.dirname(file), { recursive: true }); if (fs.existsSync(file)) fs.copyFileSync(file, file + '.bak-nextbase-' + stamp); fs.writeFileSync(file, JSON.stringify(data, null, 2) + '\n'); console.log('patched ' + file); } -const home = os.homedir(); -const provider = { groq: { baseUrl: baseUrl + '/v1/groq', apiKey: token, models: [{ id: 'openai/gpt-oss-120b', name: 'Groq GPT OSS 120B' }] } }; -for (const file of [path.join(home, '.openclaw/openclaw.json'), path.join(home, '.openclaw/agents/main/agent/models.json')]) { - const data = readJson(file, {}); - const providers = file.endsWith('openclaw.json') ? (((data.models ||= {}).providers ||= {})) : (data.providers ||= {}); - Object.assign(providers, provider); - writeJson(file, data); -} -const profilesFile = path.join(home, '.openclaw/agents/main/agent/auth-profiles.json'); -const profilesData = readJson(profilesFile, { profiles: {} }); -Object.assign((profilesData.profiles ||= {}), { 'groq:nextbase-gateway': { type: 'api_key', provider: 'groq', key: token } }); -writeJson(profilesFile, profilesData); -const stateFile = path.join(home, '.openclaw/agents/main/agent/auth-state.json'); -const state = readJson(stateFile, {}); state.lastGood ||= {}; Object.assign(state.lastGood, { groq: 'groq:nextbase-gateway' }); -state.order ||= {}; for (const [p, profile] of Object.entries({ groq: 'groq:nextbase-gateway' })) { const old = Array.isArray(state.order[p]) ? state.order[p] : []; state.order[p] = [profile, ...old.filter(x => x !== profile)]; } -writeJson(stateFile, state); -NODE -``` - -## Step 3 — Restart OCPlatform -```bash -systemctl restart openclaw-gateway.service || echo "Restart your OCPlatform process manually." -``` - -## Step 4 — Smoke test -Run this provider verify curl exactly as the gateway dashboard uses it: - -```bash -curl -sS -i \ - -H "Authorization: Bearer " \ - -H "Content-Type: application/json" \ - -d '{"model":"openai/gpt-oss-120b","messages":[{"role":"user","content":"pong"}]}' \ - http://localhost:8080/v1/groq/chat/completions -``` - -## What success looks like -- Curl returns HTTP 200. -- Response headers include `x-gateway-provider: groq` and `x-gateway-account: Groq account label`. - -## Troubleshooting -- 401 token errors usually mean the full `sp_*` secret was not pasted; the dashboard prefix alone is not enough. -- 403 or "IP not allowed" means the token has an IP allow-list that does not include this machine. -- 429 means the token, user, or upstream account hit a quota/cooldown; wait or choose another account/token. -- If OCPlatform sends `x-api-key` to Anthropic-shaped routes, set `authHeader: true` so it sends `Authorization: Bearer ...`. -- For Codex, native `agentRuntime.id: "codex"` bypasses this gateway; use the configured gateway provider/model instead. - -## Provider-specific notes -- Default model is `openai/gpt-oss-120b`. -- Streaming chat is supported when the upstream model supports it. -- Whisper STT routes are `/v1/groq/audio/transcriptions` and `/v1/groq/audio/translations`. -- Cost expectation: usually free/low-cost Groq quota until account limits or gateway caps apply. diff --git a/public/skills/hermes/SKILL.md b/public/skills/hermes/SKILL.md deleted file mode 100644 index 4fe90bc..0000000 --- a/public/skills/hermes/SKILL.md +++ /dev/null @@ -1,128 +0,0 @@ ---- -name: nextbase-hermes-setup -description: Configure the Hermes agent (NousResearch/hermes-agent) to route Anthropic, Codex, and OpenAI-compatible traffic through the Super Proxy via config.yaml custom_providers. ---- - -# Hermes via Super Proxy - -Hermes is configured very differently from OCPlatform. OpenClaw uses JSON -(`openclaw.json` + `models.json` + auth profiles); **Hermes uses a single -`~/.hermes/config.yaml`** with a `custom_providers:` list, and the gateway -token is supplied through an environment variable (`key_env`), not an inline -secret. - -This skill wires Hermes to the gateway so it can use pooled upstream accounts -(Anthropic / OpenAI Codex / Groq / Cerebras / Kimi / OpenRouter / xAI) without -carrying provider keys locally. The Anthropic path uses the gateway's Hermes -client branch, which is detected automatically — no client flag needed. - -## Prerequisites -- A running Hermes install with `~/.hermes/config.yaml`. -- A Nextbase proxy token starting with `sp_` (the user supplies it; if missing, ask). - -## Step 1 — Verify the token works -```bash -curl -sS -i \ - -H "Authorization: Bearer " \ - http://localhost:8080/v1/token/check -``` -Expect HTTP 200 with `{"ok":true,...}`. A 401 means the full `sp_*` secret -was not pasted (the dashboard prefix alone is not enough). - -## Step 2 — Put the token in the environment, not the config -Hermes reads the token from the env var named by `key_env`. Add it to -`~/.hermes/.env` (chmod 600) so it is loaded at startup and never written into -`config.yaml`: - -```bash -umask 077 -grep -q '^NEXTBASE_MODEL_GATEWAY_API_KEY=' ~/.hermes/.env 2>/dev/null \ - || echo 'NEXTBASE_MODEL_GATEWAY_API_KEY=' >> ~/.hermes/.env -``` - -## Step 3 — Add the provider to `config.yaml` under `custom_providers:` -Merge this entry into the `custom_providers:` list in `~/.hermes/config.yaml`. -For **Anthropic / Claude** the only mode that works is `anthropic_messages`: - -```yaml -custom_providers: -- name: nextbase-anthropic - base_url: http://localhost:8080 - key_env: NEXTBASE_MODEL_GATEWAY_API_KEY - api_mode: anthropic_messages - model: claude-opus-4-6 - models: - claude-opus-4-6: {} - claude-sonnet-4-5-20250929: {} - claude-haiku-4-5: {} - discover_models: false -``` - -To make a Claude model the default, point `model:` at the provider: - -```yaml -model: - default: claude-opus-4-6 - provider: custom:nextbase-anthropic -``` - -### Other gateway providers (same pattern, different base_url + api_mode) -All share `key_env: NEXTBASE_MODEL_GATEWAY_API_KEY` and `discover_models: false`. - -| name | base_url suffix | api_mode | example default model | -|---|---|---|---| -| nextbase-anthropic | `` (root) | `anthropic_messages` | claude-opus-4-6 | -| nextbase-codex | `/v1` | `codex_responses` | gpt-5.5 | -| nextbase-xai | `/v1/xai` | `codex_responses` | grok-4.3 | -| nextbase-groq | `/v1/groq` | `chat_completions` | openai/gpt-oss-120b | -| nextbase-cerebras | `/v1/cerebras` | `chat_completions` | qwen-3-235b-a22b-instruct-2507 | -| nextbase-kimi | `/v1/kimi` | `chat_completions` | k3 | -| nextbase-openrouter | `/v1/openrouter` | `chat_completions` | tencent/hy3:free | - -Full base_url = `http://localhost:8080` + the suffix. -Example for Groq: `base_url: http://localhost:8080/v1/groq`. - -## Step 4 — Restart Hermes -Restart however Hermes runs on this host (systemd unit, `hermes` process, or -docker compose). Example: -```bash -systemctl restart hermes.service 2>/dev/null \ - || (cd ~/.hermes/hermes-agent && docker compose restart) \ - || echo "Restart your Hermes process manually." -``` - -## Step 5 — Smoke test (Anthropic path) -Hit the gateway exactly as Hermes will, with a tool so the Hermes client -branch is exercised: -```bash -curl -sS -i \ - -H "Authorization: Bearer " \ - -H "Content-Type: application/json" \ - -H "anthropic-version: 2023-06-01" \ - -d '{"model":"claude-haiku-4-5","max_tokens":32,"messages":[{"role":"user","content":"reply with exactly: PONG"}]}' \ - http://localhost:8080/v1/messages -``` - -## What success looks like -- Curl returns HTTP 200. -- Response headers include `x-gateway-provider: anthropic` and `x-gateway-account: