// 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.) while // inference must go to the runtime host (bedrock-runtime.), 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 = "" // 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: ".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..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 // 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: "-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::"). // 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 }