Skip to content

Commit dcdaa33

Browse files
author
André Lange
committed
feat(catalog): overlay local route visibility on mirrored sources
1 parent 4c452c6 commit dcdaa33

16 files changed

Lines changed: 1057 additions & 44 deletions

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,17 @@
11
# fusionAIze Gate Changelog
22

3+
## v1.13.0 - Unreleased
4+
5+
### Added
6+
7+
- Expanded the provider source catalog scope beyond `blackbox`, `kilo`, and `openai` so Gate can also track mirrored official source data for `anthropic`, `deepseek`, and `google`
8+
- Added local models-endpoint overlays per configured route, which lets Gate compare what a specific key can really see against the mirrored global provider catalog
9+
10+
### Changed
11+
12+
- Provider source alerts now distinguish more clearly between global catalog drift and key-specific route/model visibility drift
13+
- Catalog summaries now include local route counts, local visible model counts, and route-vs-catalog mismatch hints instead of only source freshness and change counts
14+
315
## v1.12.0 - 2026-03-29
416

517
### Added

docs/CONFIGURATION.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,7 +384,10 @@ provider_source_refresh:
384384
timeout_seconds: 10.0
385385
interval_seconds: 21600
386386
providers:
387+
- anthropic
387388
- blackbox
389+
- deepseek
390+
- google
388391
- kilo
389392
- openai
390393
```
@@ -393,4 +396,5 @@ Notes:
393396
- startup refresh is best-effort and should not block the service if docs are unavailable
394397
- `interval_seconds` controls the conservative background refresh loop after startup
395398
- source snapshots live in the same local DB as metrics
399+
- for providers with a usable local `models` endpoint, Gate also mirrors key-specific model visibility per configured route and compares that against the global source snapshot
396400
- local billing overlays such as subscription or quota windows belong in the local account profile layer, not in the global provider snapshot

docs/FAIGATE-ROADMAP.md

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -37,29 +37,31 @@ The detailed design lives in [Adaptive model orchestration](./ADAPTIVE-ORCHESTRA
3737

3838
The next block should stay disciplined: build on the workstation baseline, keep packaging practical, and avoid turning fusionAIze Gate into a sprawling platform.
3939

40-
## Current release target: `v1.12.0`
40+
## Current release target: `v1.13.0`
4141

42-
The next release should land as a clean operational release, not as another loose pile of runtime slices.
42+
`v1.12.0` closed the first operator-facing catalog and release-hardening loop. The
43+
next release should make that catalog meaningfully more alive instead of merely
44+
more visible.
4345

44-
`v1.12.0` should close around three themes that now fit together:
46+
`v1.13.0` should close around three tightly related themes:
4547

46-
- provider source cataloging and alerting as a first-class operator surface
47-
- clearer aggregator behavior for Kilo and BLACKBOX, especially where "free", "budget", "wallet", and explicit paid lanes are easy to conflate
48-
- hardened release automation after the `v1.11.x` release failures
48+
- provider source catalog moves from mirrored docs pages to a more living operator dataset
49+
- local key and route visibility are overlaid against global provider source snapshots
50+
- provider drift gets classified more clearly as global docs drift, key-specific access drift, or route-level mismatch
4951

5052
The release should feel coherent from an operator point of view:
5153

52-
- Quick Setup, Doctor, Provider Probe, Dashboard, and route preview all explain drift or route pressure using the same language
53-
- Kilo explicit Sonnet/Opus lanes are visible as deliberate routing choices instead of hidden aggregator magic
54-
- release prep, tag validation, and publish dry-runs are boring and repeatable again
54+
- Doctor, Provider Probe, Dashboard, Quick Setup, and `/api/provider-catalog` tell the same story about what changed globally and what is only true for this key or route
55+
- free-tier, paid-tier, wallet, and BYOK assumptions are treated as per-key operational facts instead of being inferred blindly from public pricing tables
56+
- the provider source catalog becomes a trustworthy early-warning surface before route selection starts leaning on outdated assumptions
5557

56-
What is intentionally not in scope for `v1.12.0`:
58+
What is intentionally not in scope for `v1.13.0`:
5759

5860
- the virtual key layer
5961
- gateway-level response caching
60-
- fully automated external provider-source crawling on a long-running schedule
62+
- a large new bridge or client-surface expansion
6163

62-
Those stay as follow-on tracks once the operator surfaces, release path, and aggregator semantics are stable enough to trust.
64+
Those stay as follow-on tracks once the provider catalog and route-availability overlay are stable enough to trust under real operator workflows.
6365

6466
## Shipped: `v1.8.0``v1.9.1`
6567

faigate/config.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1704,9 +1704,10 @@ def _normalize_provider_source_refresh(data: dict[str, Any]) -> dict[str, Any]:
17041704
if interval_seconds <= 0:
17051705
raise ConfigError("'provider_source_refresh.interval_seconds' must be positive")
17061706

1707-
providers = raw.get("providers", ["blackbox", "kilo", "openai"])
1707+
default_providers = ["anthropic", "blackbox", "deepseek", "google", "kilo", "openai"]
1708+
providers = raw.get("providers", default_providers)
17081709
if providers in (None, ""):
1709-
providers = ["blackbox", "kilo", "openai"]
1710+
providers = default_providers
17101711
if not isinstance(providers, list) or any(
17111712
not isinstance(item, str) or not item.strip() for item in providers
17121713
):
@@ -1881,7 +1882,14 @@ def provider_source_refresh(self) -> dict:
18811882
"on_startup": True,
18821883
"timeout_seconds": 10.0,
18831884
"interval_seconds": 21600,
1884-
"providers": ["blackbox", "kilo", "openai"],
1885+
"providers": [
1886+
"anthropic",
1887+
"blackbox",
1888+
"deepseek",
1889+
"google",
1890+
"kilo",
1891+
"openai",
1892+
],
18851893
},
18861894
)
18871895

faigate/main.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@
3838
)
3939
from .lane_registry import get_provider_lane_binding, get_route_add_recommendations
4040
from .metrics import MetricsStore, calc_cost
41+
from .provider_availability import (
42+
record_availability_from_config,
43+
refresh_local_model_availability,
44+
)
4145
from .provider_catalog import (
4246
build_provider_catalog_report,
4347
build_provider_discovery_view,
@@ -74,6 +78,10 @@
7478
_provider_catalog_refresh_task: asyncio.Task[None] | None = None
7579

7680

81+
def _provider_catalog_config_path() -> str:
82+
return str(os.environ.get("FAIGATE_CONFIG_FILE") or "config.yaml")
83+
84+
7785
class PayloadTooLargeError(ValueError):
7886
"""Raised when one request or upload exceeds configured size limits."""
7987

@@ -204,6 +212,21 @@ async def _refresh_provider_source_catalog(*, force: bool = False) -> list[dict[
204212
provider_ids=target_ids,
205213
timeout_seconds=float(source_refresh_cfg.get("timeout_seconds") or 10.0),
206214
)
215+
await asyncio.to_thread(
216+
record_availability_from_config,
217+
_provider_catalog_store,
218+
config_path=_provider_catalog_config_path(),
219+
health_payload={
220+
"providers": {item["name"]: item for item in _build_provider_inventory()}
221+
},
222+
)
223+
await asyncio.to_thread(
224+
refresh_local_model_availability,
225+
_provider_catalog_store,
226+
config_path=_provider_catalog_config_path(),
227+
provider_ids=target_ids,
228+
timeout_seconds=float(source_refresh_cfg.get("timeout_seconds") or 10.0),
229+
)
207230
ok_count = sum(1 for item in refresh_results if item.ok)
208231
logger.info(
209232
"Provider source refresh completed: %s/%s source endpoints succeeded (%s)",
@@ -1908,6 +1931,14 @@ async def provider_catalog():
19081931
"priority_next": {},
19091932
}
19101933
if _provider_catalog_store is not None:
1934+
await asyncio.to_thread(
1935+
record_availability_from_config,
1936+
_provider_catalog_store,
1937+
config_path=_provider_catalog_config_path(),
1938+
health_payload={
1939+
"providers": {item["name"]: item for item in _build_provider_inventory()}
1940+
},
1941+
)
19111942
source_catalog = build_catalog_summary(
19121943
_provider_catalog_store,
19131944
provider_ids=list(_config.provider_source_refresh.get("providers") or []),

0 commit comments

Comments
 (0)