mirror of
https://github.com/netbirdio/netbird.git
synced 2026-08-25 09:01:29 +02:00
[management] Offer a provider's live model list in the config form Adds POST /api/agent-network/catalog/providers/models, which asks a vendor which models an operator's own credential can actually reach, so the provider form can offer a live list instead of only the compiled-in catalog. The catalog goes stale, and it cannot see an account: which OpenAI models an org is entitled to, which Bedrock inference profiles an account and region hold, which Vertex models a project has enabled. The endpoints, auth headers and response shapes come from probing the live APIs (#7244); each vendor invented its own envelope and none can be guessed from the request. Bedrock shaped the design: its listing lives on the control plane while inference must go to the runtime host, so Discovery carries its own host rather than reusing the record's upstream, and profile ids are taken verbatim because the region prefix is what AWS requires at invoke time. A caller supplies either the key they are typing or the id of a saved record whose stored credential is reused — never both, since accepting both would run an arbitrary credential under the identity of a record the caller may only be permitted to read. Gated on Create rather than Read, because this spends the operator's credential against a third party. Management has not made outbound calls on an operator's behalf before and it holds a credential for every provider, so every resolved address must be public — covering loopback, RFC1918, the cloud metadata address and NetBird's own 100.64/10 range — and redirects are not followed, since a redirect moves the request to a host the check never saw. The vendor is authoritative for the id; the catalog stays authoritative for pricing. A discovered model the shipped table cannot price returns pricing_known: false so the operator must set rates rather than being registered at a silent zero.
990 lines
51 KiB
Go
990 lines
51 KiB
Go
// Package catalog defines the static set of Agent Network providers
|
|
// recognized by the management server. The catalog is consulted both to
|
|
// validate provider_id on create/update and to surface the available
|
|
// providers (and their models) to the dashboard.
|
|
package catalog
|
|
|
|
import "github.com/netbirdio/netbird/shared/management/http/api"
|
|
|
|
// Model is the in-memory representation of a catalog model.
|
|
//
|
|
// The three cache rates mirror the proxy cost meter's Entry semantics
|
|
// (USD per 1k tokens; 0 = no rate configured, that bucket bills at
|
|
// InputPer1k):
|
|
// - CachedInputPer1k: OpenAI-shape rate for cached prompt tokens
|
|
// (a SUBSET of input tokens). Typically 0.1-0.5x input.
|
|
// - CacheReadPer1k / CacheCreationPer1k: Anthropic-shape rates for
|
|
// the two ADDITIVE prompt-cache buckets. Typically 0.1x / 1.25x
|
|
// input.
|
|
//
|
|
// The catalog is the single default-pricing source: the agentnetwork
|
|
// pricing package folds these models into per-surface tables that the
|
|
// synthesizer ships to the proxy's cost_meter.
|
|
type Model struct {
|
|
ID string
|
|
Label string
|
|
InputPer1k float64
|
|
OutputPer1k float64
|
|
CachedInputPer1k float64
|
|
CacheReadPer1k float64
|
|
CacheCreationPer1k float64
|
|
ContextWindow int
|
|
}
|
|
|
|
// ProviderKind groups catalog entries for UI presentation. The split
|
|
// is semantic, not technical:
|
|
// - KindProvider: the upstream is a vendor's first-party API (OpenAI,
|
|
// Anthropic, Mistral, Bedrock, etc.) — NetBird talks straight to
|
|
// the model provider.
|
|
// - KindGateway: the upstream is itself a routing / aggregation layer
|
|
// in front of multiple providers (LiteLLM, Portkey, Helicone, …).
|
|
// These typically need NetBird identity stamped onto upstream
|
|
// requests so the gateway's analytics and budgets attribute to the
|
|
// real caller; that's what IdentityInjection is for.
|
|
// - KindCustom: the catch-all "OpenAI-compatible self-hosted endpoint"
|
|
// entry (vLLM, Ollama, custom inference servers).
|
|
//
|
|
// Frontend uses Kind to group the provider Select in the modal so an
|
|
// operator can spot at a glance which catalog entries proxy other
|
|
// providers vs. talk straight to one. Backend doesn't dispatch on Kind
|
|
// today; it's purely a presentation hint.
|
|
type ProviderKind string
|
|
|
|
const (
|
|
KindProvider ProviderKind = "provider"
|
|
KindGateway ProviderKind = "gateway"
|
|
KindCustom ProviderKind = "custom"
|
|
)
|
|
|
|
// Provider is the in-memory representation of a catalog provider.
|
|
type Provider struct {
|
|
ID string
|
|
Name string
|
|
Description string
|
|
DefaultHost string
|
|
// Kind groups this entry for UI presentation; see ProviderKind.
|
|
Kind ProviderKind
|
|
// AuthHeaderName is the HTTP header the provider's API expects
|
|
// the credential under (e.g. "Authorization" for OpenAI,
|
|
// "x-api-key" for Anthropic). Combined with AuthHeaderTemplate
|
|
// at synthesis time to inject the auth header on every upstream
|
|
// request.
|
|
AuthHeaderName string
|
|
AuthHeaderTemplate string
|
|
DefaultContentType string
|
|
BrandColor string
|
|
// ParserID names the proxy LLM parser surface this provider
|
|
// speaks (matches llm.Parser.ProviderName: "openai",
|
|
// "anthropic"). Multiple catalog ids may share a parser surface
|
|
// (e.g. azure_openai_api and mistral_api both speak the OpenAI
|
|
// shape). Empty when no parser is yet implemented for the
|
|
// surface — the proxy middleware then falls back to URL sniffing
|
|
// or skips request-side enrichment.
|
|
ParserID string
|
|
// PricingSurfaces names the cost-meter pricing surfaces this
|
|
// provider's Models are priced under ("openai", "anthropic",
|
|
// "bedrock" — the llm.Parser surface the request parser stamps as
|
|
// llm.provider at billing time). NOT derivable from ParserID:
|
|
// bedrock_api and vertex_ai_api leave ParserID empty (URL-sniffed)
|
|
// yet price under "bedrock" / "anthropic", and kimi_api serves two
|
|
// body shapes so it prices under both. Nil for gateway/custom
|
|
// entries, which declare no models. Same (surface, model) pair
|
|
// contributed by two providers must carry identical rates — the
|
|
// pricing package's tests enforce that.
|
|
PricingSurfaces []string
|
|
// IdentityInjection, when non-nil, instructs the proxy to stamp
|
|
// the caller's NetBird identity onto upstream requests under the
|
|
// configured header names. Used for gateways like LiteLLM that
|
|
// key budgets and attribution off request headers (the gateway
|
|
// otherwise has no way to learn which user / group made the call).
|
|
// The proxy strips the same header names from the inbound request
|
|
// before stamping ours, so an app can't spoof identity by setting
|
|
// these headers itself.
|
|
IdentityInjection *IdentityInjection
|
|
// ExtraHeaders is a catalog-declared list of additional per-
|
|
// provider routing/config headers the proxy stamps on every
|
|
// upstream request. Distinct from AuthHeaderName/Template (which
|
|
// always carries the API_KEY) and from IdentityInjection (caller
|
|
// identity). Each entry surfaces an optional input on the
|
|
// dashboard's provider modal whose value lives on the provider
|
|
// record's ExtraValues map (keyed by ExtraHeader.Name). Empty
|
|
// list = no extra inputs rendered. Used today by Portkey for
|
|
// "x-portkey-config: pc-..." (a saved-config id that resolves
|
|
// upstream provider + credentials on Portkey's hosted side).
|
|
ExtraHeaders []ExtraHeader
|
|
Models []Model
|
|
// Discovery, when non-nil, describes how to ask this vendor which
|
|
// models the operator's own credential can actually reach, so the
|
|
// provider form can offer a live list instead of only the hand-curated
|
|
// Models above. Nil for entries with no listing endpoint (gateways
|
|
// vary too much) — those keep free-text entry.
|
|
Discovery *Discovery
|
|
}
|
|
|
|
// ListingShape names the response envelope a vendor returns its model
|
|
// listing in. Every vendor invented its own, and none of them can be
|
|
// guessed from the request, so the catalog states it.
|
|
type ListingShape string
|
|
|
|
const (
|
|
// ShapeOpenAIData is {"data":[{"id":…}]} — OpenAI, and Anthropic, which
|
|
// adopted the same envelope.
|
|
ShapeOpenAIData ListingShape = "openai_data"
|
|
// ShapeBedrockInferenceProfiles is
|
|
// {"inferenceProfileSummaries":[{"inferenceProfileId":…}]}. The ids carry
|
|
// the region prefix that makes them invocable, which is exactly what an
|
|
// operator cannot reconstruct by hand.
|
|
ShapeBedrockInferenceProfiles ListingShape = "bedrock_inference_profiles"
|
|
// ShapeVertexPublisherModels is {"publisherModels":[{"name":…}]}, where
|
|
// name is a resource path and the invocable id is its last segment joined
|
|
// to a separate versionId field.
|
|
ShapeVertexPublisherModels ListingShape = "vertex_publisher_models"
|
|
)
|
|
|
|
// Discovery describes one vendor's model-listing endpoint.
|
|
//
|
|
// Host is deliberately separate from the provider record's upstream URL:
|
|
// Bedrock serves listings from the control plane (bedrock.<region>) while
|
|
// inference must go to the runtime host (bedrock-runtime.<region>), so the
|
|
// two cannot be the same value. Empty Host means "use the record's own
|
|
// upstream", which is right for every vendor that serves both from one host.
|
|
//
|
|
// The regionPlaceholder in Host is substituted from the provider record's
|
|
// region. Deriving the discovery host from the catalog rather than accepting
|
|
// one from the caller is also what keeps this from being an open proxy: the
|
|
// only hosts management will dial are the ones written here.
|
|
type Discovery struct {
|
|
Host string
|
|
Path string
|
|
Query string
|
|
Shape ListingShape
|
|
// Headers are static headers the vendor requires beyond the credential
|
|
// (Anthropic versions its API through one and rejects a request without
|
|
// it). The auth header itself comes from AuthHeaderName/Template.
|
|
Headers map[string]string
|
|
}
|
|
|
|
// RegionPlaceholder is replaced in Discovery.Host by the provider record's
|
|
// configured region.
|
|
const RegionPlaceholder = "<region>"
|
|
|
|
// ExtraHeader names a single optional per-provider routing/config
|
|
// header. Catalog declares N of these per provider type; the operator
|
|
// fills any subset on the provider record (see Provider.ExtraValues).
|
|
// At synth time, only entries with a non-empty operator value are
|
|
// stamped; the proxy's identity-inject middleware applies anti-spoof
|
|
// (Remove + Add) so a client can't supply these headers themselves.
|
|
//
|
|
// UI copy (label / help text / tooltip) for each known Name lives on
|
|
// the dashboard, not here — the backend's job is just to declare
|
|
// which wire headers are accepted. New provider needs an extra
|
|
// header? Add the Name here AND the matching UI copy on the dashboard.
|
|
type ExtraHeader struct {
|
|
// Name is the wire header name, e.g. "x-portkey-config".
|
|
Name string
|
|
}
|
|
|
|
// IdentityInjection describes how the proxy stamps NetBird identity onto
|
|
// upstream gateway requests. Exactly one shape must be set — they're
|
|
// mutually exclusive and dispatched by the inject middleware.
|
|
//
|
|
// Shape choice tracks the wire convention the upstream gateway uses,
|
|
// not the vendor name. New gateways with a known shape become a catalog
|
|
// entry, not a new code path.
|
|
type IdentityInjection struct {
|
|
// HeaderPair emits separate headers per identity dimension
|
|
// (end-user id, tags as CSV). LiteLLM and OpenAI-compatible
|
|
// self-hosted gateways that read identity from dedicated headers.
|
|
HeaderPair *HeaderPairInjection
|
|
// JSONMetadata emits a single header carrying a JSON object with
|
|
// reserved keys for user / groups / etc. Portkey, Helicone-style
|
|
// metadata headers, anything that wants a structured envelope.
|
|
JSONMetadata *JSONMetadataInjection
|
|
}
|
|
|
|
// HeaderPairInjection is the LiteLLM-style wire convention.
|
|
type HeaderPairInjection struct {
|
|
// Customizable, when true, marks the wire header names as
|
|
// operator-overridable: the dashboard surfaces EndUserIDHeader
|
|
// and TagsHeader as editable inputs (defaults shown as
|
|
// placeholders) and the synthesizer pulls the actual values from
|
|
// the provider record's IdentityHeader* fields rather than from
|
|
// these defaults. An empty operator value disables stamping for
|
|
// that dimension. Used today for Bifrost, whose log-metadata /
|
|
// telemetry header prefix (x-bf-lh-* vs x-bf-dim-*) is a
|
|
// per-operator choice; LiteLLM and similar gateways with a fixed
|
|
// wire protocol leave this false so the catalog defaults are
|
|
// authoritative.
|
|
Customizable bool
|
|
// EndUserIDHeader receives the caller's display identity (user
|
|
// email when the peer is attached to a user, else peer.Name),
|
|
// e.g. "x-litellm-end-user-id".
|
|
EndUserIDHeader string
|
|
// TagsHeader receives the caller's NetBird group display names
|
|
// as a CSV, e.g. "x-litellm-tags".
|
|
TagsHeader string
|
|
// TagsInBody, when true, additionally writes the tag list into
|
|
// the request body's metadata.tags array (a JSON path the
|
|
// gateway parses for budget enforcement). LiteLLM only honours
|
|
// metadata.tags for tag-budget gating — its x-litellm-tags
|
|
// header path feeds spend tracking but bypasses
|
|
// _tag_max_budget_check entirely. Body inject is skipped when
|
|
// the request body is empty, truncated, non-JSON, or when an
|
|
// existing metadata field is a non-object value (defensive: we
|
|
// never clobber a client-supplied non-object). The header path
|
|
// remains a robust fallback for spend tracking in those cases.
|
|
TagsInBody bool
|
|
// EndUserIDInBody, when true, additionally writes the display
|
|
// identity into the request body's top-level "user" field (the
|
|
// OpenAI-standard end-user identifier). LiteLLM resolves the end
|
|
// user id from headers first then body, so for LiteLLM this is
|
|
// belt-and-suspenders. It matters when an OpenAI-compatible
|
|
// gateway downstream of LiteLLM (or OpenAI direct, bypassing
|
|
// LiteLLM) only reads the body, and as anti-spoof: client-
|
|
// supplied "user" values are overwritten with our trusted
|
|
// identity. Same skip rules as TagsInBody.
|
|
EndUserIDInBody bool
|
|
}
|
|
|
|
// JSONMetadataInjection is the Portkey-style wire convention: a single
|
|
// header carrying a JSON object. NetBird identity fields land under the
|
|
// configured reserved keys; missing keys (empty string) are skipped at
|
|
// emit time.
|
|
type JSONMetadataInjection struct {
|
|
// Customizable, when true, marks the JSON keys as operator-
|
|
// overridable. The dashboard surfaces UserKey and GroupsKey as
|
|
// editable inputs (the catalog values shown as placeholders) and
|
|
// the synthesizer pulls the actual JSON-key names from the
|
|
// provider record's IdentityHeader* fields. Same field reuse as
|
|
// HeaderPair's customizable path — the dimensions (user identity,
|
|
// groups) are the same, only the wire encoding differs (JSON key
|
|
// vs HTTP header name). An empty operator value disables emission
|
|
// for that dimension. Used today for Cloudflare AI Gateway, whose
|
|
// cf-aig-metadata header accepts arbitrary JSON keys; Portkey
|
|
// leaves this false because its keys are reserved by the Portkey
|
|
// schema.
|
|
Customizable bool
|
|
// Header is the wire header name carrying the JSON payload, e.g.
|
|
// "x-portkey-metadata".
|
|
Header string
|
|
// UserKey is the JSON key for the caller's display identity.
|
|
// Portkey reserves "_user" for this dimension.
|
|
UserKey string
|
|
// GroupsKey is the JSON key for the caller's NetBird groups,
|
|
// emitted as a CSV string value (Portkey requires string values).
|
|
GroupsKey string
|
|
// MaxValueLength caps each emitted JSON value, in bytes. Portkey
|
|
// enforces a 128-char limit per value; oversized values are
|
|
// truncated rather than failing the request. 0 disables the cap.
|
|
MaxValueLength int
|
|
// Sanitize, when true, replaces characters outside the destination's
|
|
// accepted set with '_' before emitting each value. AWS Bedrock's
|
|
// X-Amzn-Bedrock-Request-Metadata restricts values to a limited character
|
|
// class, so unsanitized group display names (e.g. containing spaces) would
|
|
// make Bedrock reject the request with 400.
|
|
Sanitize bool
|
|
}
|
|
|
|
// providers is the canonical list of supported Agent Network providers.
|
|
// Update this list together with the dashboard's PROVIDER_CATALOG.
|
|
var providers = []Provider{
|
|
{
|
|
ID: "openai_api",
|
|
Kind: KindProvider,
|
|
Name: "OpenAI API",
|
|
Description: "GPT, Responses API, and Embeddings",
|
|
DefaultHost: "api.openai.com",
|
|
AuthHeaderName: "Authorization",
|
|
AuthHeaderTemplate: "Bearer ${API_KEY}",
|
|
DefaultContentType: "application/json",
|
|
BrandColor: "#10A37F",
|
|
Discovery: &Discovery{
|
|
Path: "/v1/models",
|
|
Shape: ShapeOpenAIData,
|
|
},
|
|
ParserID: "openai",
|
|
PricingSurfaces: []string{"openai"},
|
|
// Pricing + context windows cross-checked against LiteLLM's
|
|
// model_prices_and_context_window.json. Notable corrections from
|
|
// earlier values: o4-mini repriced from $4/$16 to $1.10/$4.40
|
|
// per MTok, gpt-4o from $5/$15 to $2.50/$10, and the GPT-5
|
|
// family context windows split between 1.05M for full-size
|
|
// models and 272K for mini/nano/codex variants.
|
|
Models: []Model{
|
|
{ID: "gpt-5.5", Label: "GPT-5.5", InputPer1k: 0.005, OutputPer1k: 0.030, CachedInputPer1k: 0.0005, ContextWindow: 1050000},
|
|
{ID: "gpt-5.5-pro", Label: "GPT-5.5 Pro", InputPer1k: 0.030, OutputPer1k: 0.180, CachedInputPer1k: 0.003, ContextWindow: 1050000},
|
|
{ID: "gpt-5.4", Label: "GPT-5.4", InputPer1k: 0.0025, OutputPer1k: 0.015, CachedInputPer1k: 0.00025, ContextWindow: 1050000},
|
|
{ID: "gpt-5.4-pro", Label: "GPT-5.4 Pro", InputPer1k: 0.030, OutputPer1k: 0.180, CachedInputPer1k: 0.003, ContextWindow: 1050000},
|
|
{ID: "gpt-5.4-mini", Label: "GPT-5.4 Mini", InputPer1k: 0.00075, OutputPer1k: 0.0045, CachedInputPer1k: 0.000075, ContextWindow: 272000},
|
|
{ID: "gpt-5.4-nano", Label: "GPT-5.4 Nano", InputPer1k: 0.0002, OutputPer1k: 0.00125, CachedInputPer1k: 0.00002, ContextWindow: 272000},
|
|
{ID: "gpt-5.3-codex", Label: "GPT-5.3 Codex", InputPer1k: 0.00175, OutputPer1k: 0.014, CachedInputPer1k: 0.000175, ContextWindow: 272000},
|
|
{ID: "gpt-5.3-chat-latest", Label: "GPT-5.3 Chat", InputPer1k: 0.00175, OutputPer1k: 0.014, CachedInputPer1k: 0.000175, ContextWindow: 128000},
|
|
{ID: "o4-mini", Label: "o4-mini", InputPer1k: 0.0011, OutputPer1k: 0.0044, CachedInputPer1k: 0.000275, ContextWindow: 200000},
|
|
{ID: "gpt-4.1", Label: "GPT-4.1", InputPer1k: 0.002, OutputPer1k: 0.008, CachedInputPer1k: 0.0005, ContextWindow: 1047576},
|
|
{ID: "gpt-4.1-mini", Label: "GPT-4.1 mini", InputPer1k: 0.0004, OutputPer1k: 0.0016, CachedInputPer1k: 0.0001, ContextWindow: 1047576},
|
|
{ID: "gpt-4.1-nano", Label: "GPT-4.1 nano", InputPer1k: 0.0001, OutputPer1k: 0.0004, CachedInputPer1k: 0.000025, ContextWindow: 1047576},
|
|
{ID: "gpt-4o", Label: "GPT-4o", InputPer1k: 0.0025, OutputPer1k: 0.010, CachedInputPer1k: 0.00125, ContextWindow: 128000},
|
|
{ID: "gpt-4o-mini", Label: "GPT-4o mini", InputPer1k: 0.00015, OutputPer1k: 0.0006, CachedInputPer1k: 0.000075, ContextWindow: 128000},
|
|
{ID: "gpt-4-turbo", Label: "GPT-4 Turbo", InputPer1k: 0.01, OutputPer1k: 0.03, ContextWindow: 128000},
|
|
{ID: "gpt-3.5-turbo", Label: "GPT-3.5 Turbo", InputPer1k: 0.0005, OutputPer1k: 0.0015, ContextWindow: 16385},
|
|
{ID: "text-embedding-3-large", Label: "text-embedding-3-large", InputPer1k: 0.00013, OutputPer1k: 0, ContextWindow: 8191},
|
|
{ID: "text-embedding-3-small", Label: "text-embedding-3-small", InputPer1k: 0.00002, OutputPer1k: 0, ContextWindow: 8191},
|
|
},
|
|
},
|
|
{
|
|
ID: "anthropic_api",
|
|
Kind: KindProvider,
|
|
Name: "Anthropic API",
|
|
Description: "Claude Messages API",
|
|
DefaultHost: "api.anthropic.com",
|
|
AuthHeaderName: "x-api-key",
|
|
AuthHeaderTemplate: "${API_KEY}",
|
|
DefaultContentType: "application/json",
|
|
BrandColor: "#D97757",
|
|
Discovery: &Discovery{
|
|
Path: "/v1/models",
|
|
// The default page is short and a picker wants the whole
|
|
// catalogue in one call.
|
|
Query: "limit=1000",
|
|
Shape: ShapeOpenAIData,
|
|
// Anthropic versions its API through a header and refuses a
|
|
// request that omits it, listing included.
|
|
Headers: map[string]string{"anthropic-version": "2023-06-01"},
|
|
},
|
|
ParserID: "anthropic",
|
|
PricingSurfaces: []string{"anthropic"},
|
|
// Per Anthropic's current model lineup. Pricing in USD per 1k
|
|
// tokens. Context windows: 4.6+ family is 1M; Haiku 4.5 stays at
|
|
// 200K. claude-3-7-sonnet and claude-3-5-haiku retired
|
|
// 2026-02-19 — dropped from the catalog. claude-opus-4-1
|
|
// deprecated, retires 2026-08-05 — kept until the cutover.
|
|
// claude-mythos-5 omitted: Project Glasswing access only, not a
|
|
// general-availability target. claude-fable-5 requires the
|
|
// account to be on >= 30-day data retention or all requests
|
|
// 400.
|
|
Models: []Model{
|
|
{ID: "claude-opus-5", Label: "Claude Opus 5", InputPer1k: 0.005, OutputPer1k: 0.025, CacheReadPer1k: 0.0005, CacheCreationPer1k: 0.00625, ContextWindow: 1000000},
|
|
{ID: "claude-sonnet-5", Label: "Claude Sonnet 5", InputPer1k: 0.003, OutputPer1k: 0.015, CacheReadPer1k: 0.0003, CacheCreationPer1k: 0.00375, ContextWindow: 1000000},
|
|
{ID: "claude-fable-5", Label: "Claude Fable 5", InputPer1k: 0.010, OutputPer1k: 0.050, CacheReadPer1k: 0.001, CacheCreationPer1k: 0.0125, ContextWindow: 1000000},
|
|
{ID: "claude-opus-4-8", Label: "Claude Opus 4.8", InputPer1k: 0.005, OutputPer1k: 0.025, CacheReadPer1k: 0.0005, CacheCreationPer1k: 0.00625, ContextWindow: 1000000},
|
|
{ID: "claude-opus-4-7", Label: "Claude Opus 4.7", InputPer1k: 0.005, OutputPer1k: 0.025, CacheReadPer1k: 0.0005, CacheCreationPer1k: 0.00625, ContextWindow: 1000000},
|
|
{ID: "claude-opus-4-6", Label: "Claude Opus 4.6", InputPer1k: 0.005, OutputPer1k: 0.025, CacheReadPer1k: 0.0005, CacheCreationPer1k: 0.00625, ContextWindow: 1000000},
|
|
{ID: "claude-opus-4-1", Label: "Claude Opus 4.1 (deprecated, retires 2026-08-05)", InputPer1k: 0.015, OutputPer1k: 0.075, CacheReadPer1k: 0.0015, CacheCreationPer1k: 0.01875, ContextWindow: 200000},
|
|
{ID: "claude-sonnet-4-6", Label: "Claude Sonnet 4.6", InputPer1k: 0.003, OutputPer1k: 0.015, CacheReadPer1k: 0.0003, CacheCreationPer1k: 0.00375, ContextWindow: 1000000},
|
|
{ID: "claude-sonnet-4-5", Label: "Claude Sonnet 4.5", InputPer1k: 0.003, OutputPer1k: 0.015, CacheReadPer1k: 0.0003, CacheCreationPer1k: 0.00375, ContextWindow: 200000},
|
|
{ID: "claude-haiku-4-5", Label: "Claude Haiku 4.5", InputPer1k: 0.001, OutputPer1k: 0.005, CacheReadPer1k: 0.0001, CacheCreationPer1k: 0.00125, ContextWindow: 200000},
|
|
},
|
|
},
|
|
{
|
|
ID: "azure_openai_api",
|
|
Kind: KindProvider,
|
|
Name: "Azure OpenAI API",
|
|
Description: "Azure-hosted OpenAI deployments",
|
|
DefaultHost: "<resource>.openai.azure.com",
|
|
AuthHeaderName: "api-key",
|
|
AuthHeaderTemplate: "${API_KEY}",
|
|
DefaultContentType: "application/json",
|
|
BrandColor: "#0078D4",
|
|
ParserID: "openai",
|
|
PricingSurfaces: []string{"openai"},
|
|
// Mirrors openai_api pricing — Azure resells OpenAI models at the
|
|
// same per-token rates, just under different deployment names.
|
|
Models: []Model{
|
|
{ID: "gpt-5.5", Label: "GPT-5.5 (Azure)", InputPer1k: 0.005, OutputPer1k: 0.030, CachedInputPer1k: 0.0005, ContextWindow: 1050000},
|
|
{ID: "gpt-5.4", Label: "GPT-5.4 (Azure)", InputPer1k: 0.0025, OutputPer1k: 0.015, CachedInputPer1k: 0.00025, ContextWindow: 1050000},
|
|
{ID: "gpt-5.4-mini", Label: "GPT-5.4 Mini (Azure)", InputPer1k: 0.00075, OutputPer1k: 0.0045, CachedInputPer1k: 0.000075, ContextWindow: 272000},
|
|
{ID: "gpt-5.4-nano", Label: "GPT-5.4 Nano (Azure)", InputPer1k: 0.0002, OutputPer1k: 0.00125, CachedInputPer1k: 0.00002, ContextWindow: 272000},
|
|
{ID: "o4-mini", Label: "o4-mini (Azure)", InputPer1k: 0.0011, OutputPer1k: 0.0044, CachedInputPer1k: 0.000275, ContextWindow: 200000},
|
|
{ID: "gpt-4.1", Label: "GPT-4.1 (Azure)", InputPer1k: 0.002, OutputPer1k: 0.008, CachedInputPer1k: 0.0005, ContextWindow: 1047576},
|
|
{ID: "gpt-4.1-mini", Label: "GPT-4.1 mini (Azure)", InputPer1k: 0.0004, OutputPer1k: 0.0016, CachedInputPer1k: 0.0001, ContextWindow: 1047576},
|
|
{ID: "gpt-4o", Label: "GPT-4o (Azure)", InputPer1k: 0.0025, OutputPer1k: 0.010, CachedInputPer1k: 0.00125, ContextWindow: 128000},
|
|
{ID: "gpt-4o-mini", Label: "GPT-4o mini (Azure)", InputPer1k: 0.00015, OutputPer1k: 0.0006, CachedInputPer1k: 0.000075, ContextWindow: 128000},
|
|
{ID: "gpt-35-turbo", Label: "GPT-3.5 Turbo (Azure)", InputPer1k: 0.0005, OutputPer1k: 0.0015, ContextWindow: 16385},
|
|
},
|
|
},
|
|
{
|
|
ID: "bedrock_api",
|
|
Kind: KindProvider,
|
|
Name: "AWS Bedrock API",
|
|
Description: "Anthropic, Meta, Cohere via Bedrock",
|
|
DefaultHost: "bedrock-runtime.<region>.amazonaws.com",
|
|
AuthHeaderName: "Authorization",
|
|
AuthHeaderTemplate: "Bearer ${API_KEY}",
|
|
DefaultContentType: "application/json",
|
|
BrandColor: "#FF9900",
|
|
// Listings come from the CONTROL PLANE, not the runtime host in
|
|
// DefaultHost above: ListInferenceProfiles is not an operation
|
|
// bedrock-runtime implements, and answers <UnknownOperationException/>
|
|
// there. Inference has to go to the runtime host, so the two hosts
|
|
// genuinely differ and Discovery.Host carries the difference.
|
|
//
|
|
// Inference profiles rather than foundation models because the profile
|
|
// id is the invocable one: it carries the region prefix (eu., us.,
|
|
// global.) that AWS requires and that cannot be derived from the
|
|
// configured region — an eu-central-1 account legitimately holds
|
|
// global.* profiles.
|
|
Discovery: &Discovery{
|
|
Host: "bedrock." + RegionPlaceholder + ".amazonaws.com",
|
|
Path: "/inference-profiles",
|
|
Shape: ShapeBedrockInferenceProfiles,
|
|
},
|
|
// ParserID stays empty (path-style dispatch via IsBedrockPathStyle);
|
|
// the request parser meters these under the "bedrock" surface.
|
|
PricingSurfaces: []string{"bedrock"},
|
|
// Anthropic models on Bedrock take the anthropic.* prefix and
|
|
// follow the same lineup / pricing as the first-party Anthropic
|
|
// catalog entry above. claude-3-7-sonnet and claude-3-5-haiku
|
|
// were retired upstream on 2026-02-19 — dropped from the
|
|
// Bedrock list too. Amazon Nova entries cross-checked against
|
|
// LiteLLM (added Nova Micro + the new Nova 2 Lite preview).
|
|
// Llama 3.3 70B entry kept unchanged — LiteLLM tracks only
|
|
// per-region Llama 3 entries; standalone 3.3 not yet listed.
|
|
Models: []Model{
|
|
{ID: "anthropic.claude-opus-5", Label: "Claude Opus 5 (Bedrock)", InputPer1k: 0.005, OutputPer1k: 0.025, CacheReadPer1k: 0.0005, CacheCreationPer1k: 0.00625, ContextWindow: 1000000},
|
|
{ID: "anthropic.claude-sonnet-5", Label: "Claude Sonnet 5 (Bedrock)", InputPer1k: 0.003, OutputPer1k: 0.015, CacheReadPer1k: 0.0003, CacheCreationPer1k: 0.00375, ContextWindow: 1000000},
|
|
{ID: "anthropic.claude-opus-4-8", Label: "Claude Opus 4.8 (Bedrock)", InputPer1k: 0.005, OutputPer1k: 0.025, CacheReadPer1k: 0.0005, CacheCreationPer1k: 0.00625, ContextWindow: 1000000},
|
|
{ID: "anthropic.claude-opus-4-7", Label: "Claude Opus 4.7 (Bedrock)", InputPer1k: 0.005, OutputPer1k: 0.025, CacheReadPer1k: 0.0005, CacheCreationPer1k: 0.00625, ContextWindow: 1000000},
|
|
{ID: "anthropic.claude-opus-4-6", Label: "Claude Opus 4.6 (Bedrock)", InputPer1k: 0.005, OutputPer1k: 0.025, CacheReadPer1k: 0.0005, CacheCreationPer1k: 0.00625, ContextWindow: 1000000},
|
|
{ID: "anthropic.claude-opus-4-1", Label: "Claude Opus 4.1 (Bedrock, deprecated 2026-08-05)", InputPer1k: 0.015, OutputPer1k: 0.075, CacheReadPer1k: 0.0015, CacheCreationPer1k: 0.01875, ContextWindow: 200000},
|
|
{ID: "anthropic.claude-sonnet-4-6", Label: "Claude Sonnet 4.6 (Bedrock)", InputPer1k: 0.003, OutputPer1k: 0.015, CacheReadPer1k: 0.0003, CacheCreationPer1k: 0.00375, ContextWindow: 1000000},
|
|
{ID: "anthropic.claude-sonnet-4-5", Label: "Claude Sonnet 4.5 (Bedrock)", InputPer1k: 0.003, OutputPer1k: 0.015, CacheReadPer1k: 0.0003, CacheCreationPer1k: 0.00375, ContextWindow: 200000},
|
|
{ID: "anthropic.claude-haiku-4-5", Label: "Claude Haiku 4.5 (Bedrock)", InputPer1k: 0.001, OutputPer1k: 0.005, CacheReadPer1k: 0.0001, CacheCreationPer1k: 0.00125, ContextWindow: 200000},
|
|
{ID: "meta.llama3-3-70b-instruct", Label: "Llama 3.3 70B (Bedrock)", InputPer1k: 0.00072, OutputPer1k: 0.00072, ContextWindow: 128000},
|
|
{ID: "amazon.nova-2-lite", Label: "Amazon Nova 2 Lite (Bedrock, preview)", InputPer1k: 0.0003, OutputPer1k: 0.0025, ContextWindow: 1000000},
|
|
{ID: "amazon.nova-pro", Label: "Amazon Nova Pro (Bedrock)", InputPer1k: 0.0008, OutputPer1k: 0.0032, ContextWindow: 300000},
|
|
{ID: "amazon.nova-lite", Label: "Amazon Nova Lite (Bedrock)", InputPer1k: 0.00006, OutputPer1k: 0.00024, ContextWindow: 300000},
|
|
{ID: "amazon.nova-micro", Label: "Amazon Nova Micro (Bedrock)", InputPer1k: 0.000035, OutputPer1k: 0.00014, ContextWindow: 128000},
|
|
},
|
|
// Bedrock accepts a cost-allocation metadata header; stamp the caller's
|
|
// user + authorizing group so spend can be attributed in AWS Cost
|
|
// Management. Sanitized because Bedrock restricts the value character set.
|
|
IdentityInjection: &IdentityInjection{
|
|
JSONMetadata: &JSONMetadataInjection{
|
|
Header: "X-Amzn-Bedrock-Request-Metadata",
|
|
UserKey: "user",
|
|
GroupsKey: "group",
|
|
MaxValueLength: 256,
|
|
Sanitize: true,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
ID: "vertex_ai_api",
|
|
Kind: KindProvider,
|
|
Name: "Google Vertex AI API",
|
|
Description: "Anthropic Claude models hosted on Vertex AI",
|
|
DefaultHost: "<region>-aiplatform.googleapis.com",
|
|
AuthHeaderName: "Authorization",
|
|
AuthHeaderTemplate: "Bearer ${API_KEY}",
|
|
DefaultContentType: "application/json",
|
|
BrandColor: "#4285F4",
|
|
// Only the v1beta1 publisher listing answers: the v1 form and the
|
|
// project-scoped form under BOTH versions return 404. That means the
|
|
// list is publisher-global — it cannot say which models this project
|
|
// has enabled — so it is offered as a suggestion beside the catalog
|
|
// rather than replacing it. See the discovery e2e for the probes.
|
|
Discovery: &Discovery{
|
|
Path: "/v1beta1/publishers/anthropic/models",
|
|
Shape: ShapeVertexPublisherModels,
|
|
},
|
|
// ParserID stays empty (path-style dispatch via IsVertexPathStyle);
|
|
// Anthropic-on-Vertex requests are metered under the "anthropic"
|
|
// surface with the bare, unversioned model id.
|
|
PricingSurfaces: []string{"anthropic"},
|
|
// Vertex carries the model in the URL path and authenticates with a
|
|
// service-account-minted OAuth token (api_key = "keyfile::<base64 SA>").
|
|
// Only Anthropic-on-Vertex is metered today: the request parser maps the
|
|
// anthropic publisher to the Anthropic parser, so the lineup + prices
|
|
// mirror the first-party Anthropic catalog (LiteLLM vertex_ai/claude-*
|
|
// confirms the same per-token rates; cross-region profiles in eu/apac
|
|
// carry a ~10% premium that base pricing does not model). Gemini (the
|
|
// google publisher) is intentionally omitted until a Gemini parser
|
|
// exists — the router denies unmeterable publishers rather than forward
|
|
// them uncounted.
|
|
Models: []Model{
|
|
{ID: "claude-opus-5", Label: "Claude Opus 5 (Vertex)", InputPer1k: 0.005, OutputPer1k: 0.025, CacheReadPer1k: 0.0005, CacheCreationPer1k: 0.00625, ContextWindow: 1000000},
|
|
{ID: "claude-sonnet-5", Label: "Claude Sonnet 5 (Vertex)", InputPer1k: 0.003, OutputPer1k: 0.015, CacheReadPer1k: 0.0003, CacheCreationPer1k: 0.00375, ContextWindow: 1000000},
|
|
{ID: "claude-fable-5", Label: "Claude Fable 5 (Vertex)", InputPer1k: 0.010, OutputPer1k: 0.050, CacheReadPer1k: 0.001, CacheCreationPer1k: 0.0125, ContextWindow: 1000000},
|
|
{ID: "claude-opus-4-8", Label: "Claude Opus 4.8 (Vertex)", InputPer1k: 0.005, OutputPer1k: 0.025, CacheReadPer1k: 0.0005, CacheCreationPer1k: 0.00625, ContextWindow: 1000000},
|
|
{ID: "claude-opus-4-7", Label: "Claude Opus 4.7 (Vertex)", InputPer1k: 0.005, OutputPer1k: 0.025, CacheReadPer1k: 0.0005, CacheCreationPer1k: 0.00625, ContextWindow: 1000000},
|
|
{ID: "claude-opus-4-6", Label: "Claude Opus 4.6 (Vertex)", InputPer1k: 0.005, OutputPer1k: 0.025, CacheReadPer1k: 0.0005, CacheCreationPer1k: 0.00625, ContextWindow: 1000000},
|
|
{ID: "claude-opus-4-1", Label: "Claude Opus 4.1 (Vertex, deprecated 2026-08-05)", InputPer1k: 0.015, OutputPer1k: 0.075, CacheReadPer1k: 0.0015, CacheCreationPer1k: 0.01875, ContextWindow: 200000},
|
|
{ID: "claude-sonnet-4-6", Label: "Claude Sonnet 4.6 (Vertex)", InputPer1k: 0.003, OutputPer1k: 0.015, CacheReadPer1k: 0.0003, CacheCreationPer1k: 0.00375, ContextWindow: 1000000},
|
|
{ID: "claude-sonnet-4-5", Label: "Claude Sonnet 4.5 (Vertex)", InputPer1k: 0.003, OutputPer1k: 0.015, CacheReadPer1k: 0.0003, CacheCreationPer1k: 0.00375, ContextWindow: 200000},
|
|
{ID: "claude-haiku-4-5", Label: "Claude Haiku 4.5 (Vertex)", InputPer1k: 0.001, OutputPer1k: 0.005, CacheReadPer1k: 0.0001, CacheCreationPer1k: 0.00125, ContextWindow: 200000},
|
|
},
|
|
},
|
|
{
|
|
ID: "mistral_api",
|
|
Kind: KindProvider,
|
|
Name: "Mistral API",
|
|
Description: "Mistral cloud API",
|
|
DefaultHost: "api.mistral.ai",
|
|
AuthHeaderName: "Authorization",
|
|
AuthHeaderTemplate: "Bearer ${API_KEY}",
|
|
DefaultContentType: "application/json",
|
|
BrandColor: "#FF7000",
|
|
ParserID: "openai",
|
|
PricingSurfaces: []string{"openai"},
|
|
// Pricing + context windows cross-checked against LiteLLM. Key
|
|
// gotchas the marketing page hides:
|
|
// - `mistral-medium-latest` aliases to Medium 3.1 ($0.40/$2),
|
|
// NOT Medium 3.5 ($1.50/$7.50). Catalog exposes both.
|
|
// - `mistral-large-latest` aliases to Large 3 — 262K context,
|
|
// cheaper than Medium 3.5.
|
|
// - Magistral models are tuned for reasoning but cap context
|
|
// at only 40K (vs 128K-262K elsewhere).
|
|
// - `codestral-latest` still routes to the old 2405 build
|
|
// ($1/$3) per LiteLLM; the newer codestral-2508 is both
|
|
// cheaper and longer-context. Both exposed.
|
|
// - Pixtral was folded into the main Large/Medium series; no
|
|
// standalone vision entry.
|
|
Models: []Model{
|
|
{ID: "mistral-large-latest", Label: "Mistral Large 3", InputPer1k: 0.0005, OutputPer1k: 0.0015, ContextWindow: 262144},
|
|
{ID: "mistral-medium-latest", Label: "Mistral Medium 3.1", InputPer1k: 0.0004, OutputPer1k: 0.002, ContextWindow: 131072},
|
|
{ID: "mistral-medium-3-5", Label: "Mistral Medium 3.5", InputPer1k: 0.0015, OutputPer1k: 0.0075, ContextWindow: 262144},
|
|
{ID: "mistral-small-latest", Label: "Mistral Small 3.2", InputPer1k: 0.00006, OutputPer1k: 0.00018, ContextWindow: 131072},
|
|
{ID: "magistral-medium-latest", Label: "Magistral Medium (reasoning)", InputPer1k: 0.002, OutputPer1k: 0.005, ContextWindow: 40000},
|
|
{ID: "magistral-small-latest", Label: "Magistral Small (reasoning)", InputPer1k: 0.0005, OutputPer1k: 0.0015, ContextWindow: 40000},
|
|
{ID: "devstral-medium-latest", Label: "Devstral Medium 2 (coding)", InputPer1k: 0.0004, OutputPer1k: 0.002, ContextWindow: 256000},
|
|
{ID: "devstral-small-latest", Label: "Devstral Small 2 (coding)", InputPer1k: 0.0001, OutputPer1k: 0.0003, ContextWindow: 256000},
|
|
{ID: "codestral-2508", Label: "Codestral 2508", InputPer1k: 0.0003, OutputPer1k: 0.0009, ContextWindow: 256000},
|
|
{ID: "codestral-latest", Label: "Codestral (legacy 2405)", InputPer1k: 0.001, OutputPer1k: 0.003, ContextWindow: 32000},
|
|
{ID: "ministral-3-14b-2512", Label: "Ministral 3 14B", InputPer1k: 0.0002, OutputPer1k: 0.0002, ContextWindow: 262144},
|
|
{ID: "ministral-8b-latest", Label: "Ministral 8B", InputPer1k: 0.00015, OutputPer1k: 0.00015, ContextWindow: 262144},
|
|
{ID: "ministral-3-3b-2512", Label: "Ministral 3 3B", InputPer1k: 0.0001, OutputPer1k: 0.0001, ContextWindow: 131072},
|
|
{ID: "mistral-embed", Label: "Mistral Embed", InputPer1k: 0.0001, OutputPer1k: 0, ContextWindow: 8192},
|
|
},
|
|
},
|
|
{
|
|
ID: "kimi_api",
|
|
Kind: KindProvider,
|
|
Name: "Kimi (Moonshot AI) API",
|
|
Description: "Kimi K3 / K2 models via the Moonshot AI platform",
|
|
DefaultHost: "api.moonshot.ai",
|
|
AuthHeaderName: "Authorization",
|
|
AuthHeaderTemplate: "Bearer ${API_KEY}",
|
|
DefaultContentType: "application/json",
|
|
BrandColor: "#1A1A2E",
|
|
// ParserID empty on purpose: Moonshot serves two body shapes on
|
|
// the same host and key, and the proxy's URL sniffer dispatches
|
|
// both (same pattern as Bifrost). /v1/chat/completions matches
|
|
// OpenAIParser; the Anthropic-compatible endpoint the official
|
|
// Claude Code guide uses (/anthropic/v1/messages) contains
|
|
// "/v1/messages" and matches AnthropicParser. Pinning "openai"
|
|
// here would misparse the Claude Code path — the primary way
|
|
// teams consume Kimi for coding today. Both endpoints accept the
|
|
// same Moonshot key via Authorization: Bearer (Claude Code's
|
|
// ANTHROPIC_AUTH_TOKEN rides that header too).
|
|
//
|
|
// api.moonshot.ai is the international platform; mainland-China
|
|
// accounts live on api.moonshot.cn with separate billing —
|
|
// operators there override the host on the provider record. The
|
|
// kimi.com subscription coding endpoint (api.kimi.com/coding,
|
|
// model id "k3") is account-bound seat licensing rather than a
|
|
// meterable platform key, so it's deliberately not the default.
|
|
ParserID: "",
|
|
// Both body shapes are metered: /v1/chat/completions under
|
|
// "openai", /anthropic/v1/messages under "anthropic" — so the
|
|
// K3 entry is priced on both surfaces.
|
|
PricingSurfaces: []string{"openai", "anthropic"},
|
|
// Pricing per Moonshot's platform rates at K3 launch (July 2026):
|
|
// $3/$15 per MTok with $0.30 cached input, flat across the 1M-token
|
|
// window. kimi-k3 is the ONLY model the platform serves newer
|
|
// accounts — K2-era ids (kimi-k2-thinking) and even the kimi-latest
|
|
// alias return resource_not_found_error, verified live 2026-07-21 —
|
|
// so it's the only catalog entry. Grandfathered accounts with K2
|
|
// access can still type those ids on the provider's model rows.
|
|
// The consumer app's "K3 Swarm Max" mode is not an API SKU, so it
|
|
// doesn't appear here.
|
|
Models: []Model{
|
|
// Carries both cache shapes: Moonshot reports cache hits
|
|
// OpenAI-style on /v1/chat/completions (CachedInputPer1k)
|
|
// and Anthropic-style on /anthropic/v1/messages
|
|
// (CacheReadPer1k) — $0.30/MTok either way. Each surface's
|
|
// cost formula reads only its own field, so the superset
|
|
// entry prices both endpoints correctly. No cache-creation
|
|
// rate published; writes bill at the input rate.
|
|
{ID: "kimi-k3", Label: "Kimi K3", InputPer1k: 0.003, OutputPer1k: 0.015, CachedInputPer1k: 0.0003, CacheReadPer1k: 0.0003, ContextWindow: 1000000},
|
|
},
|
|
},
|
|
{
|
|
ID: "litellm_proxy",
|
|
Kind: KindGateway,
|
|
Name: "LiteLLM Proxy",
|
|
Description: "Bring your own LiteLLM proxy with NetBird identity stamped on every request",
|
|
DefaultHost: "",
|
|
AuthHeaderName: "Authorization",
|
|
AuthHeaderTemplate: "Bearer ${API_KEY}",
|
|
DefaultContentType: "application/json",
|
|
BrandColor: "#0EA5E9",
|
|
ParserID: "openai",
|
|
// IdentityInjection requires a LiteLLM virtual key minted with
|
|
// metadata.allow_client_tags=true; the master key silently drops
|
|
// caller tags. Tags go out via both the x-litellm-tags header and
|
|
// body metadata.tags: LiteLLM enforces budgets from the body only,
|
|
// so the header is the spend-tracking fallback when body injection
|
|
// can't run. See the Agent Network provider docs for key setup.
|
|
IdentityInjection: &IdentityInjection{
|
|
HeaderPair: &HeaderPairInjection{
|
|
EndUserIDHeader: "x-litellm-end-user-id",
|
|
TagsHeader: "x-litellm-tags",
|
|
TagsInBody: true,
|
|
EndUserIDInBody: true,
|
|
},
|
|
},
|
|
Models: []Model{},
|
|
},
|
|
{
|
|
ID: "portkey",
|
|
Kind: KindGateway,
|
|
Name: "Portkey AI Gateway",
|
|
Description: "Portkey AI Gateway with NetBird identity stamped via x-portkey-metadata",
|
|
DefaultHost: "api.portkey.ai",
|
|
// Portkey hosted requires x-portkey-api-key (account key)
|
|
// plus a routing decision per request. The simplest routing
|
|
// path is a saved Portkey config id stamped via
|
|
// x-portkey-config — operators paste the pc-... id once and
|
|
// Portkey resolves the upstream provider + virtual key from
|
|
// it. ExtraHeaders below surfaces the input. Alternative:
|
|
// callers author "@org/model" in the body; both flows
|
|
// coexist (per-request authoring still works without a
|
|
// configured value).
|
|
AuthHeaderName: "x-portkey-api-key",
|
|
AuthHeaderTemplate: "${API_KEY}",
|
|
DefaultContentType: "application/json",
|
|
BrandColor: "#FF5C00",
|
|
ParserID: "openai",
|
|
IdentityInjection: &IdentityInjection{
|
|
JSONMetadata: &JSONMetadataInjection{
|
|
Header: "x-portkey-metadata",
|
|
UserKey: "_user",
|
|
GroupsKey: "groups",
|
|
MaxValueLength: 128,
|
|
},
|
|
},
|
|
ExtraHeaders: []ExtraHeader{
|
|
{Name: "x-portkey-config"},
|
|
},
|
|
Models: []Model{},
|
|
},
|
|
{
|
|
ID: "bifrost",
|
|
Kind: KindGateway,
|
|
Name: "Bifrost",
|
|
Description: "Maxim AI's Bifrost gateway. Point upstream URL at /openai/v1 or /anthropic/v1 on your Bifrost host depending on which body shape your apps use.",
|
|
DefaultHost: "",
|
|
AuthHeaderName: "Authorization",
|
|
AuthHeaderTemplate: "Bearer ${API_KEY}",
|
|
DefaultContentType: "application/json",
|
|
BrandColor: "#7C3AED",
|
|
// ParserID empty: the proxy's request parser sniffs the URL
|
|
// path. Bifrost's /openai/v1/... contains "/v1/chat/completions"
|
|
// (matches OpenAIParser.DetectFromURL); /anthropic/v1/messages
|
|
// contains "/v1/messages" (matches AnthropicParser). Operators
|
|
// who paste a different prefix get no usage parsing and the
|
|
// cost meter skips with skipMissingProvider — degraded but
|
|
// non-fatal.
|
|
ParserID: "",
|
|
// Identity-injection headers are operator-customisable. The
|
|
// HeaderPair values below are PLACEHOLDERS surfaced by the
|
|
// dashboard; the actual values stamped on the wire come from
|
|
// the provider record's IdentityHeaderUserID /
|
|
// IdentityHeaderGroups fields. An empty operator value
|
|
// disables stamping for that dimension (the inject middleware
|
|
// already no-ops on empty header names). Defaulting to the
|
|
// x-bf-dim- family so the values land in Bifrost's
|
|
// Prometheus/OTEL pipelines when the operator declares the
|
|
// label names in their client.prometheus_labels config — see
|
|
// docs.getbifrost.ai/features/telemetry. Operators who use
|
|
// the always-on x-bf-lh- log-metadata family (no Bifrost-side
|
|
// declaration required) just edit the inputs.
|
|
//
|
|
// Bifrost virtual keys (sk-bf-*) ride Authorization: Bearer.
|
|
// Operators provision the VK on their Bifrost (UI /
|
|
// config.json / POST /api/governance/virtual-keys) and paste
|
|
// the returned sk-bf-... as ${API_KEY}. Pin v1.4+ to avoid
|
|
// the v1.3.0 x-bf-vk regression (maximhq/bifrost#632).
|
|
IdentityInjection: &IdentityInjection{
|
|
HeaderPair: &HeaderPairInjection{
|
|
EndUserIDHeader: "x-bf-dim-netbird_user_id",
|
|
TagsHeader: "x-bf-dim-netbird_groups",
|
|
Customizable: true,
|
|
},
|
|
},
|
|
Models: []Model{},
|
|
},
|
|
{
|
|
ID: "cloudflare_ai_gateway",
|
|
Kind: KindGateway,
|
|
Name: "Cloudflare AI Gateway",
|
|
Description: "Cloudflare AI Gateway. Operator pastes the gateway URL (with the upstream provider slug like /openai or /anthropic so the URL sniffer dispatches to the right parser) and a per-gateway authentication token. Recommended setup is BYOK / Stored Keys: Cloudflare manages the upstream provider credential and the gateway token is the only secret NetBird needs.",
|
|
DefaultHost: "",
|
|
AuthHeaderName: "cf-aig-authorization",
|
|
AuthHeaderTemplate: "Bearer ${API_KEY}",
|
|
DefaultContentType: "application/json",
|
|
BrandColor: "#F38020",
|
|
// ParserID empty: like Bifrost, the proxy's parser-detect
|
|
// sniffs the URL path. /openai/... contains the OpenAI hint
|
|
// substrings; /anthropic/v1/messages contains /v1/messages
|
|
// (matches AnthropicParser). The /compat universal endpoint
|
|
// also speaks OpenAI shape so OpenAIParser handles it.
|
|
// Operators who paste a different prefix degrade to no-cost
|
|
// (skipMissingProvider) but the request still flows.
|
|
ParserID: "",
|
|
// cf-aig-metadata is a single header carrying a JSON object;
|
|
// up to five string/number/boolean values per request. NetBird
|
|
// occupies two slots (user id + groups CSV) and leaves three
|
|
// for operator-added context. JSON keys are operator-
|
|
// customisable so Cloudflare-side log filters can use the
|
|
// operator's existing label conventions instead of NetBird's
|
|
// defaults — hence Customizable=true. The dashboard surfaces
|
|
// the catalog values as placeholders; only the values stored
|
|
// on the provider record's IdentityHeader* fields land on the
|
|
// wire (empty operator value = key is omitted from the JSON,
|
|
// since applyJSONMetadata already skips empty keys).
|
|
IdentityInjection: &IdentityInjection{
|
|
JSONMetadata: &JSONMetadataInjection{
|
|
Header: "cf-aig-metadata",
|
|
UserKey: "netbird_user_id",
|
|
GroupsKey: "netbird_groups",
|
|
Customizable: true,
|
|
// Cloudflare's docs don't specify a per-value cap;
|
|
// leaving 0 disables the truncate path. Header-level
|
|
// constraint is "5 entries max" rather than length.
|
|
MaxValueLength: 0,
|
|
},
|
|
},
|
|
Models: []Model{},
|
|
},
|
|
{
|
|
ID: "vercel_ai_gateway",
|
|
Kind: KindGateway,
|
|
Name: "Vercel AI Gateway",
|
|
Description: "Vercel's unified API for hundreds of models. Single endpoint, OpenAI-compatible body, model dispatch via prefix (openai/..., anthropic/..., google/..., xai/...). Per-user / per-tag attribution lands in Vercel's Custom Reporting API and observability dashboard.",
|
|
DefaultHost: "",
|
|
AuthHeaderName: "Authorization",
|
|
AuthHeaderTemplate: "Bearer ${API_KEY}",
|
|
DefaultContentType: "application/json",
|
|
BrandColor: "#000000",
|
|
// Vercel always speaks OpenAI shape on /v1/chat/completions —
|
|
// the model prefix in the body picks the upstream provider.
|
|
// No URL sniffing needed; pin the parser directly.
|
|
ParserID: "openai",
|
|
// HeaderPair shape with fixed wire names dictated by Vercel's
|
|
// Custom Reporting API contract. Customizable=false because
|
|
// renaming the headers makes Vercel silently stop attributing
|
|
// — the gateway's reporting endpoint only matches its own
|
|
// header names. Same fixed-protocol position as LiteLLM.
|
|
//
|
|
// Caveats operators should know:
|
|
// - up to 10 tags total per request (deduped); 11+ → HTTP 400
|
|
// - each tag must be 1-64 chars
|
|
// - user up to 256 chars (NetBird user emails fit)
|
|
// - $0.075 per 1k unique user/tag values written
|
|
// We don't enforce the caps in the inject middleware today;
|
|
// operators in groups beyond the 10-tag limit will see Vercel
|
|
// 400s and need to re-scope their group memberships.
|
|
IdentityInjection: &IdentityInjection{
|
|
HeaderPair: &HeaderPairInjection{
|
|
EndUserIDHeader: "ai-reporting-user",
|
|
TagsHeader: "ai-reporting-tags",
|
|
},
|
|
},
|
|
Models: []Model{},
|
|
},
|
|
{
|
|
ID: "openrouter",
|
|
Kind: KindGateway,
|
|
Name: "OpenRouter",
|
|
Description: "OpenRouter's unified API for hundreds of models. Single endpoint at openrouter.ai/api/v1, OpenAI-compatible body, model dispatch via prefix (anthropic/claude-..., openai/gpt-..., google/gemini-..., etc.). Per-user attribution lands in OpenRouter's analytics via the OpenAI-standard `user` body field; OpenRouter has no groups / tags dimension at request time.",
|
|
DefaultHost: "openrouter.ai/api/v1",
|
|
AuthHeaderName: "Authorization",
|
|
AuthHeaderTemplate: "Bearer ${API_KEY}",
|
|
DefaultContentType: "application/json",
|
|
BrandColor: "#6F4FF2",
|
|
// OpenRouter is single-endpoint OpenAI-shape on /api/v1/chat/completions —
|
|
// model prefix in the body picks the upstream provider.
|
|
// Pinning the parser saves URL sniffing.
|
|
ParserID: "openai",
|
|
// HeaderPair shape with EndUserIDInBody as the only active
|
|
// dimension. OpenRouter's per-user attribution is the
|
|
// OpenAI-standard `user` body field, not a header — and
|
|
// OpenRouter offers no per-request groups / tags dimension at
|
|
// all. Customizable=false because the field name is locked by
|
|
// OpenAI's spec; renaming would just defeat the inject.
|
|
IdentityInjection: &IdentityInjection{
|
|
HeaderPair: &HeaderPairInjection{
|
|
EndUserIDInBody: true,
|
|
},
|
|
},
|
|
// HTTP-Referer + X-OpenRouter-Title surface in OpenRouter's
|
|
// app rankings and per-app analytics. Operators paste their
|
|
// own app URL + display name on the provider record so their
|
|
// requests show under their brand instead of "no app". Both
|
|
// are static per-deployment, not per-request, hence the
|
|
// ExtraHeaders mechanism (operator-typed value, stamped on
|
|
// every request to this provider). Skip X-OpenRouter-Categories
|
|
// for now — the marketplace-categories dimension is
|
|
// niche-enough that we'd add it on demand.
|
|
ExtraHeaders: []ExtraHeader{
|
|
{Name: "HTTP-Referer"},
|
|
{Name: "X-OpenRouter-Title"},
|
|
},
|
|
Models: []Model{},
|
|
},
|
|
{
|
|
// vLLM is an OpenAI-compatible self-hosted server. It behaves like
|
|
// the generic custom entry; it gets its own catalog id purely so it
|
|
// surfaces as a named "vLLM" choice in the provider picker.
|
|
ID: "vllm",
|
|
Kind: KindCustom,
|
|
Name: "vLLM",
|
|
Description: "Self-hosted vLLM (OpenAI-compatible)",
|
|
DefaultHost: "",
|
|
AuthHeaderName: "Authorization",
|
|
AuthHeaderTemplate: "Bearer ${API_KEY}",
|
|
DefaultContentType: "application/json",
|
|
BrandColor: "#30A2FF",
|
|
Models: []Model{},
|
|
},
|
|
{
|
|
ID: "custom",
|
|
Kind: KindCustom,
|
|
Name: "Custom / Self-hosted",
|
|
Description: "OpenAI-compatible endpoint (vLLM, Ollama, …)",
|
|
DefaultHost: "",
|
|
AuthHeaderName: "Authorization",
|
|
AuthHeaderTemplate: "Bearer ${API_KEY}",
|
|
DefaultContentType: "application/json",
|
|
BrandColor: "#9CA3AF",
|
|
Models: []Model{},
|
|
},
|
|
}
|
|
|
|
// All returns a copy of the full catalog.
|
|
func All() []Provider {
|
|
out := make([]Provider, len(providers))
|
|
copy(out, providers)
|
|
return out
|
|
}
|
|
|
|
// Lookup returns the catalog entry with the given id, if any.
|
|
func Lookup(id string) (Provider, bool) {
|
|
for _, p := range providers {
|
|
if p.ID == id {
|
|
return p, true
|
|
}
|
|
}
|
|
return Provider{}, false
|
|
}
|
|
|
|
// IsKnown reports whether the given id refers to a catalog entry.
|
|
func IsKnown(id string) bool {
|
|
_, ok := Lookup(id)
|
|
return ok
|
|
}
|
|
|
|
// IsVertexPathStyle reports whether a provider uses the Google Vertex AI
|
|
// request shape — the model is carried in the URL path
|
|
// (/v1/projects/{p}/locations/{r}/publishers/{pub}/models/{model}:{action})
|
|
// rather than the body, so the proxy routes it by path instead of by model.
|
|
func IsVertexPathStyle(providerID string) bool {
|
|
return providerID == "vertex_ai_api"
|
|
}
|
|
|
|
// IsBedrockPathStyle reports whether a provider uses the AWS Bedrock request
|
|
// shape — the model is carried in the URL path (/model/{modelId}/{action},
|
|
// action being invoke, invoke-with-response-stream, converse, or
|
|
// converse-stream) rather than the body, so the proxy routes it by path.
|
|
func IsBedrockPathStyle(providerID string) bool {
|
|
return providerID == "bedrock_api"
|
|
}
|
|
|
|
// ToAPIResponse renders a catalog provider as the API representation.
|
|
func (p Provider) ToAPIResponse() api.AgentNetworkCatalogProvider {
|
|
models := make([]api.AgentNetworkCatalogModel, 0, len(p.Models))
|
|
for _, m := range p.Models {
|
|
am := api.AgentNetworkCatalogModel{
|
|
Id: m.ID,
|
|
Label: m.Label,
|
|
InputPer1k: m.InputPer1k,
|
|
OutputPer1k: m.OutputPer1k,
|
|
ContextWindow: m.ContextWindow,
|
|
}
|
|
// Cache rates are emitted only when configured so the dashboard
|
|
// can prefill them; 0 stays off the wire (absent = no rate).
|
|
if m.CachedInputPer1k > 0 {
|
|
v := m.CachedInputPer1k
|
|
am.CachedInputPer1k = &v
|
|
}
|
|
if m.CacheReadPer1k > 0 {
|
|
v := m.CacheReadPer1k
|
|
am.CacheReadPer1k = &v
|
|
}
|
|
if m.CacheCreationPer1k > 0 {
|
|
v := m.CacheCreationPer1k
|
|
am.CacheCreationPer1k = &v
|
|
}
|
|
models = append(models, am)
|
|
}
|
|
kind := api.AgentNetworkCatalogProviderKindProvider
|
|
switch p.Kind {
|
|
case KindGateway:
|
|
kind = api.AgentNetworkCatalogProviderKindGateway
|
|
case KindCustom:
|
|
kind = api.AgentNetworkCatalogProviderKindCustom
|
|
}
|
|
resp := api.AgentNetworkCatalogProvider{
|
|
Id: p.ID,
|
|
Name: p.Name,
|
|
Description: p.Description,
|
|
DefaultHost: p.DefaultHost,
|
|
Kind: kind,
|
|
AuthHeaderTemplate: p.AuthHeaderTemplate,
|
|
DefaultContentType: p.DefaultContentType,
|
|
BrandColor: p.BrandColor,
|
|
Models: models,
|
|
}
|
|
if len(p.PricingSurfaces) > 0 {
|
|
surfaces := append([]string(nil), p.PricingSurfaces...)
|
|
resp.PricingSurfaces = &surfaces
|
|
}
|
|
if len(p.ExtraHeaders) > 0 {
|
|
extras := make([]api.AgentNetworkCatalogExtraHeader, 0, len(p.ExtraHeaders))
|
|
for _, h := range p.ExtraHeaders {
|
|
extras = append(extras, api.AgentNetworkCatalogExtraHeader{
|
|
Name: h.Name,
|
|
})
|
|
}
|
|
resp.ExtraHeaders = &extras
|
|
}
|
|
// Surface IdentityInjection so the dashboard can decide whether
|
|
// to render editable inputs vs. a read-only mappings strip per
|
|
// shape's customizable flag. HeaderPair (Bifrost) and
|
|
// JSONMetadata (Cloudflare, Portkey) are mutually exclusive on a
|
|
// given catalog entry; emit whichever shape is set.
|
|
if p.IdentityInjection != nil {
|
|
injection := &api.AgentNetworkCatalogIdentityInjection{}
|
|
if hp := p.IdentityInjection.HeaderPair; hp != nil {
|
|
injection.HeaderPair = &api.AgentNetworkCatalogHeaderPairInjection{
|
|
Customizable: hp.Customizable,
|
|
EndUserIdHeader: hp.EndUserIDHeader,
|
|
TagsHeader: hp.TagsHeader,
|
|
}
|
|
}
|
|
if jm := p.IdentityInjection.JSONMetadata; jm != nil {
|
|
injection.JsonMetadata = &api.AgentNetworkCatalogJSONMetadataInjection{
|
|
Customizable: jm.Customizable,
|
|
Header: jm.Header,
|
|
UserKey: jm.UserKey,
|
|
GroupsKey: jm.GroupsKey,
|
|
}
|
|
}
|
|
if injection.HeaderPair != nil || injection.JsonMetadata != nil {
|
|
resp.IdentityInjection = injection
|
|
}
|
|
}
|
|
return resp
|
|
}
|