CLIProxyAPI plugin that injects identity headers into requests routed to Antigravity, agy2api, or gpt2api providers. It also exposes provider matching diagnostics through the CPA Management API.
The plugin ID is now any2api-bridge. The legacy
agy-identity-bridge config key is still accepted during migration, so an
existing CPA configuration can be loaded and migrated by the next save.
For a matched request the plugin:
- Reads the selected request context after CPA authentication.
- Derives an opaque SHA-256 principal from the Bearer token.
- Detects the client application from
X-AGY-Client-Appor a small, conservative User-Agent classifier. - Adds
X-AGY-Principal,X-AGY-Client-App, and optionallyX-AGY-Signature.
The plugin does not modify unrelated providers.
Canonical contract and operations notes live in .docs/README.md.
Release 0.3.0 keeps the identity bridge canonical payload stable for agy2api
while preserving the legacy signing fallback during the transition period,
adds the renamed any2api-bridge plugin identity, and keeps passive usage
telemetry for the mirrored provider.
- Back up CPA
config.yaml. - Install/update to
any2api-bridgev0.3.0. - Keep the old
plugins.configs.agy-identity-bridgeblock long enough for the new plugin to merge it, or manually rename the key toplugins.configs.any2api-bridge. - Disable/remove the old
agy-identity-bridgeplugin file after the new plugin is effective so both copies cannot publish the same models. - Restart or reload CPA and verify the plugin list shows
any2api-bridge.
Use the canonical CPA shape:
plugins:
enabled: true
configs:
any2api-bridge:
enabled: true
priority: 100
auto_discover: true
include_native_antigravity: true
match_mode: any
match_name: ""
match_url: ""
match_api_key: ""
match_provider: ""
match_providers: []
match_model: "agy/*"
match_models: []
hmac_secret_source: envCLIProxyAPI calls the after-auth interceptor with pluginapi.RequestInterceptRequest,
which carries the source format, the selected upstream model, the client
requested model and the request headers. It does not carry the provider
name or base URL for OpenAI-compatible providers, and ToFormat is just
openai for all of them.
The one field that does identify the provider is the requested model prefix.
So the plugin reads openai-compatibility from the mounted CPA config, keeps
the providers your selectors match, and maps a live request back to its
provider by that provider's prefix. A request for agy/gemini-3.5-flash-low
is therefore attributed to the provider whose prefix: agy.
That means a provider you want to affect must have a prefix in CPA config.
The status endpoint lists what it found under active_prefixes and warns when
a matched provider has none, because such a provider cannot be identified per
request.
When all match_* selectors are empty and auto_discover is enabled, the
plugin matches:
- native CPA requests whose
ToFormatisantigravity; - requests whose model carries the prefix of a provider that auto discovery matched;
- providers whose name, provider key, or base URL contains
antigravityoragy2api.
Explicit selectors support case-insensitive substring matching and */?
wildcards:
match_name: provider name;match_url: provider base URL;match_api_key: exact provider API key;match_provider: one provider name/key selector (legacy-compatible alias);match_providers: additional provider name/key patterns;match_model: requested model, for exampleagy/*;match_models: additional requested model patterns;match_mode: any: at least one configured selector must match;match_mode: all: every configured selector must match.
match_name and match_url describe which config providers the plugin owns
and shape the diagnostics view. Add match_model when you want the request
path pinned explicitly as well, which is the most precise option.
match_api_key is a selector only. It is not silently reused as the HMAC
secret.
CLIProxyAPI's OpenAI-compatible executor builds its upstream request from scratch and never applies the headers an interceptor returned, so identity headers injected by this plugin cannot reach agy2api through that provider. Executor mode removes that limitation by making this plugin the caller.
plugins:
configs:
any2api-bridge:
executor_enabled: true
executor_provider: ln.Antigravity
model_namespace: "spike."The plugin mirrors the provider it matches in openai-compatibility: base URL,
API keys, extra headers, priority, disable-cooling, prefix and the model list.
Model metadata is mapped the same way the host does it, so alias wins over name
and image: true becomes the openai-image type. The bridge preserves
supported_reasoning_efforts from the upstream catalog, including none as a
real selectable level. It only supplies the contract-defined levels for the
exact family aliases gemini-3.1-pro, gemini-3.6-flash, gemini-3.7-flash,
gemini-3.8-flash, and gemini-image; concrete suffixed IDs and unknown
models remain unannotated unless the provider declares metadata. Nothing is
silently substituted: if the provider declares no models, the plugin publishes
none.
With model_namespace: "", the mirrored models keep the original provider
prefix. For example, an original prefix: agy publishes
agy/gemini-3.7-flash-high through the plugin after cutover. The executor
removes that prefix before forwarding the upstream request to agy2api. Setting
model_namespace overrides the original prefix for a parallel test.
The mirrored provider keeps the original's priority as a base and adds a boost:
priority = max(original priority + 100, 10). Today CLIProxyAPI ignores
priority when two providers publish the same model ID, but if a future version
starts honoring it, the plugin provider wins instead of splitting traffic.
The plugin returns model names without a provider prefix during model
registration. CPA applies the prefix from the plugin-owned auth record, so the
visible client model remains agy/<model> (or the configured namespace)
without becoming agy/agy/<model>. A family alias is routable only after it is
present in the mirrored provider model list or the plugin's fetched model cache;
agy2api advertising it alone cannot satisfy CPA's pre-executor model lookup.
CPA normalizes a dynamic suffix such as gemini-3.8-flash(high) before provider
lookup and carries the effort in request metadata. The plugin preserves that
contract by forwarding the base model plus reasoning_effort=high to agy2api.
An explicit effort already present in the request body wins over the suffix.
Image-lane model names, including gemini-image, are forwarded after prefix
stripping only: the plugin does not add reasoning effort or rewrite an image
model suffix, so chat requests for gemini-image reach agy2api unchanged.
CPA's public /v1/models response currently exposes only basic model fields,
so effort controls are not discoverable through CPA until CPA itself forwards
that metadata.
For streaming, when CPA supplies a bridge stream_id, the plugin returns from
executor.execute_stream immediately and continues pumping upstream SSE events
through host.stream.emit in a goroutine. CPA's RPC adapter cannot expose the
bridge channel to the HTTP response writer until the executor call returns, so
returning late turns progressive emit into full-response batching.
If the upstream stream starts with a non-2xx status, the plugin does not treat
the response as SSE. It drains a bounded body, closes the host HTTP stream once,
and returns an upstream_error envelope with CPA's supported http_status
field. This preserves concrete upstream failures such as agy2api HTTP 413
instead of letting CPA report a misleading empty_stream.
CPA calls both interceptor phases. The plugin handles request.intercept_before
as an intentional no-op because provider identity and the selected client
credential are only available in the after-auth phase, where the identity
headers are added.
executor_enabled defaults to false, and installing a new plugin version does
not change routing on its own.
While the mirrored provider is still enabled, the plugin withholds its models
unless model_namespace is set. Two providers publishing the same model ID would
make CLIProxyAPI load balance across them, and the requests that land on the
original provider silently lose their identity. The status endpoint reports
models_served and warns while this is in effect.
To finish the switch, disable the mirrored provider in CPA config and clear
model_namespace. The plugin then publishes the same visible model names
clients already use after CPA applies the preserved provider prefix.
The status endpoint reports replacement_mode so the operator always knows
which path serves a model:
withheld: executor is on, the original provider is still enabled, andmodel_namespaceis empty. The plugin publishes nothing; the original provider serves all traffic.namespace: executor is on andmodel_namespaceis set. The plugin publishes the mirrored models under the namespace prefix for parallel testing while the original keeps serving the un-prefixed names.active: executor is on,model_namespaceis empty, and the original provider is disabled in CPA config. The plugin is the only serving path for these models; the status page warns that re-enabling the original provider is required before disabling or uninstalling the plugin.
provider_original_enabled mirrors the original provider's disabled flag so
the transition can be verified from diagnostics alone. CPA notifies the plugin
through plugin.reconfigure whenever config.yaml changes, so disabling the
original provider in the CPA UI takes effect without a restart.
The signing key is resolved in strict priority order:
agy2api_identity_secret: the dedicated plugin config field. When set, it wins over every other source, including an explicitnone.hmac_secret: the legacy plugin config field.AGY_PLUGIN_SECRET: the CPA container environment variable.provider_api_key: only whenhmac_secret_source: provider_api_keyand no stronger source has a value.
agy2api verifies the signature on every request, so an unsigned request is
rejected with 401. When the executor sees that happen, diagnostics add a
signature mismatch warning and record the failing status in
last_executor_status, which usually means the plugin secret and the agy2api
AGY_IDENTITY_BRIDGE_SECRET do not match.
The dedicated field is write-only: GET /settings and diagnostics report
agy2api_identity_secret_configured: true but never the value itself.
The legacy hmac_secret_source selector still exists for compatibility:
plugins:
configs:
any2api-bridge:
hmac_secret_source: envIts remaining effect: provider_api_key signs with the selected provider API
key when no stronger source is configured (not available for native OAuth
auths), and none suppresses signing only when no secret is configured
anywhere. Prefer setting agy2api_identity_secret (or AGY_PLUGIN_SECRET) and
leaving the selector alone.
The receiver must use the same signature contract. The plugin never returns the secret or raw provider API keys in diagnostics.
The plugin normally mirrors the configured provider without changing it. The
Provider View dashboard can also save deliberate edits back to config.yaml;
API keys and the agy2api identity secret are write-only there, so leaving those
fields empty preserves the existing values.
- Re-enable the original
Antigravityprovider in CPA (openai-compatibility, toggle the provider back on). CPA reloads the config, the plugin receivesplugin.reconfigure, the collision guard re-engages, and the plugin withholds its models again. Traffic returns to CPA's built-in OpenAI executor with the latest saved provider settings. - Optionally set
executor_enabled: falsein the plugin config (or disable the plugin) to remove the executor path entirely. Models published by the plugin disappear; the interceptor keeps running so identity headers keep working for the original provider path.
Order matters: re-enable the original provider before disabling or
uninstalling the plugin. While replacement_mode is active, the plugin is the
only serving path for the mirrored models, and removing it without re-enabling
the original would take those models offline.
Authenticated Management API routes:
GET /v0/management/plugins/any2api-bridge/status
GET /v0/management/plugins/any2api-bridge/provider
GET /v0/management/plugins/any2api-bridge/provider/config
POST /v0/management/plugins/any2api-bridge/provider/save
POST /v0/management/plugins/any2api-bridge/provider/test
POST /v0/management/plugins/any2api-bridge/provider/fetch-models
GET /v0/management/plugins/any2api-bridge/settings
POST /v0/management/plugins/any2api-bridge/rescan
The status response reports:
- whether the mounted CPA config was found;
- whether this plugin has a config block;
- whether the plugin instance is enabled and its configured priority;
- the active matching mode and number of explicit selectors;
- separate counts for configured records and runtime auth records;
- number of records and unique providers matched;
- the provider names and redacted URLs affected by the plugin, with match reasons;
- the complete scanned provider list on the authenticated route, including unmatched records;
- whether API keys are configured, without returning their values;
- warnings for native OAuth/API-key selector limitations.
providers contains only records the plugin will affect. scanned_providers
contains both matched and unmatched records so a wrong selector can be
diagnosed without guessing.
CPA also exposes a redacted browser resource:
/v0/resource/plugins/any2api-bridge/status
/v0/resource/plugins/any2api-bridge/provider
/v0/resource/plugins/any2api-bridge/usage
These resources intentionally omit config paths, URLs, auth indexes, and credential values until a management key is supplied in the Provider View. CPA serves plugin resource routes without management authentication, so account labels (native Antigravity auth labels are account emails) are also stripped from this projection. Use the authenticated status route when you need them.
The Any2Api Bridge resource is a single dashboard for the mirrored
ln.Antigravity path. Its main surface keeps the operational state, live model
IDs, runtime log, usage filters, token totals, and compact usage analysis
visible. Provider configuration and deeper usage analysis open in one wider
right-side drawer with Provider and Usage analytics tabs.
The provider tab has the same core fields as an OpenAI-compatible provider: name, base URL, prefix, priority, disabled state, disable cooling, headers, API keys, and custom models. API key entries, headers, models, and thinking levels can be collapsed independently. Saving requires the CPA management key and writes only the selected mirrored provider plus this plugin's executor settings.
Usage analytics reads usage data from agy2api responses when present, keeps
the response body and headers untouched for other trackers, and groups the
statistics by period, minute/hour/day/week/month bucket, source, model share,
client source, cache performance, and recent observations. The period presets
are Last 5 hours, Last 7 days, Last 30 days, Current month, and
All time.
User-Agent identifies the transport layer more reliably than the originating
application. For example, openai/python 2.24.0 is recorded as
openai-python; it must not be interpreted as Hermes. The plugin cannot
reliably recover the original application from that adapter string.
When agy2api must apply app-specific connector, skill, or MCP policy, the
originating client should send a trusted explicit identity header such as
X-AGY-Client-App: hermes, preferably with
X-AGY-Client-Instance and X-AGY-Capability-Profile. The plugin preserves
those explicit values, includes them in the stable principal/signature
contract, and never guesses Hermes from openai/python. If no explicit
identity is available, use a separate client key or session context for each
application installation.
The Configured, Registered, and Inactive labels in CPA Manager describe
the host lifecycle, not provider matching:
Configured:plugins.configs.any2api-bridgeexists inconfig.yaml.Registered: CPA loaded the dynamic library and acceptedplugin.register.Effective: globalplugins.enabled, per-pluginenabled, and registration are all active.Inactive: usually global plugins are disabled, the per-plugin flag is false, or registration failed.Not registered: CPA did not load the binary, the path/architecture is wrong, or CPA needs a restart.
Provider matching details are available from the authenticated status route after the plugin is registered.
The release artifact is a Linux amd64 CGO shared library. Build on Linux with Go 1.26 and GCC:
make test
make build VERSION=0.3.0The output is:
dist/any2api-bridge-v0.3.0.so
The GitHub Actions workflow builds and packages:
any2api-bridge_0.3.0_linux_amd64.zip
checksums.txt
Add the registry source to CPA:
plugins:
store-sources:
- https://raw.githubusercontent.com/ngojclee/any2api-bridge/main/registry.jsonAfter a tagged release, refresh the CPA Plugin Store and restart CPA after installation so the dynamic library is registered.
go vet ./...
go test ./...MIT