mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-02 12:52:17 +03:00
Compare commits
13 Commits
docs/opena
...
feat/provi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c32be09e6a | ||
|
|
63e4afa321 | ||
|
|
7ca5e1c671 | ||
|
|
73db936f98 | ||
|
|
4b5266d3f8 | ||
|
|
f8b01c966e | ||
|
|
e12fb110f9 | ||
|
|
2fbd0f5c25 | ||
|
|
18c71b91dc | ||
|
|
9392bd55c2 | ||
|
|
90366903c4 | ||
|
|
668beed5b8 | ||
|
|
298ad0fd64 |
12
AGENTS.md
12
AGENTS.md
@@ -131,10 +131,14 @@ breaker runs on `circuitBreakerThreshold` / `circuitBreakerReset`:
|
||||
| API key | `7` | `12` | `30s` |
|
||||
| Local | (derived) | `2` | `15s` |
|
||||
|
||||
`PROVIDER_PROFILES` also defines `providerFailureThreshold` (10/15/2) and `providerCooldownMs`
|
||||
(5min/10min/1min); those fields are loaded into the profile but have **no runtime consumer
|
||||
today** — do not tune or document them as the live breaker. Every default is overridable
|
||||
through the `OMNIROUTE_PROVIDER_BREAKER_*` and `OMNIROUTE_CIRCUIT_BREAKER_*` env vars; the
|
||||
`PROVIDER_PROFILES` also defines `providerFailureThreshold` (10/15/2),
|
||||
`providerFailureWindowMs` (15/30/5 min) and `providerCooldownMs` (5/10/1 min): these power the
|
||||
**window gate of the opt-in global Provider Cooldown** (`PROVIDER_COOLDOWN_ENABLED`, default
|
||||
off) — a provider-level entry in `open-sse/services/providerCooldownTracker.ts` only counts as
|
||||
cooling after `providerFailureThreshold` failures inside `providerFailureWindowMs`, and then
|
||||
cools for `providerCooldownMs`. They are NOT the live breaker's thresholds — do not tune them
|
||||
expecting breaker behavior. Every default is overridable through the
|
||||
`OMNIROUTE_PROVIDER_BREAKER_*` and `OMNIROUTE_CIRCUIT_BREAKER_*` env vars; the
|
||||
runtime-accurate reference table lives in `docs/architecture/RESILIENCE_GUIDE.md`.
|
||||
|
||||
Only provider-level failure statuses should trip the provider breaker:
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
- **feat(routing):** add a `score` Auto router strategy that selects the highest configured weighted score and reuses `explorationRate`.
|
||||
@@ -0,0 +1,5 @@
|
||||
- Keep the embedding registry's vector width and `embedding` type on models when a synced model exists
|
||||
for the same id, so `/v1/models` no longer reports registry-described embedding models widthless or
|
||||
untyped (#11761)
|
||||
- Correct `google/gemini-embedding-001` on the OpenRouter route to 3072 dimensions, the width it
|
||||
returns when `dimensions` is not sent (#11761)
|
||||
@@ -0,0 +1 @@
|
||||
- Absorb `Error [AbortError]: request_signal_aborted` and DOMException AbortError shapes in the process-level client-abort crash guard so routine client disconnects no longer kill the server (exit code 7).
|
||||
@@ -829,12 +829,6 @@
|
||||
"src/app/(dashboard)/dashboard/HomePageClient.tsx": {
|
||||
"@typescript-eslint/no-unused-vars": {
|
||||
"count": 2
|
||||
},
|
||||
"react-hooks/exhaustive-deps": {
|
||||
"count": 1
|
||||
},
|
||||
"react-hooks/set-state-in-effect": {
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"src/app/(dashboard)/dashboard/a2a/page.tsx": {
|
||||
@@ -850,62 +844,16 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/app/(dashboard)/dashboard/activity/ActivityFeedClient.tsx": {
|
||||
"react-hooks/purity": {
|
||||
"count": 1
|
||||
},
|
||||
"react-hooks/refs": {
|
||||
"count": 1
|
||||
},
|
||||
"react-hooks/set-state-in-effect": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/app/(dashboard)/dashboard/analytics/CacheHealthTab.tsx": {
|
||||
"react-hooks/set-state-in-effect": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/app/(dashboard)/dashboard/analytics/ComboHealthTab.tsx": {
|
||||
"react-hooks/set-state-in-effect": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/app/(dashboard)/dashboard/analytics/CompressionAnalyticsTab.tsx": {
|
||||
"@typescript-eslint/no-unused-vars": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/app/(dashboard)/dashboard/analytics/ProviderUtilizationTab.tsx": {
|
||||
"react-hooks/set-state-in-effect": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/app/(dashboard)/dashboard/analytics/RouteExplainabilityTab.tsx": {
|
||||
"react-hooks/set-state-in-effect": {
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"src/app/(dashboard)/dashboard/api-manager/ApiManagerPageClient.tsx": {
|
||||
"@typescript-eslint/no-unused-vars": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/app/(dashboard)/dashboard/audit/A2aAuditTab.tsx": {
|
||||
"react-hooks/set-state-in-effect": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/app/(dashboard)/dashboard/audit/ComplianceTab.tsx": {
|
||||
"react-hooks/set-state-in-effect": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/app/(dashboard)/dashboard/audit/McpAuditTab.tsx": {
|
||||
"react-hooks/set-state-in-effect": {
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"src/app/(dashboard)/dashboard/batch/components/wizard/CostEstimateStep.tsx": {
|
||||
"react-hooks/set-state-in-effect": {
|
||||
"count": 1
|
||||
@@ -926,24 +874,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/app/(dashboard)/dashboard/cache/components/CacheEntriesTab.tsx": {
|
||||
"react-hooks/set-state-in-effect": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/app/(dashboard)/dashboard/cache/components/ReasoningCacheTab.tsx": {
|
||||
"react-hooks/purity": {
|
||||
"count": 1
|
||||
},
|
||||
"react-hooks/set-state-in-effect": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/app/(dashboard)/dashboard/cache/page.tsx": {
|
||||
"react-hooks/set-state-in-effect": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/app/(dashboard)/dashboard/cli-agents/CliAgentsPageClient.tsx": {
|
||||
"no-restricted-syntax": {
|
||||
"count": 4
|
||||
@@ -1060,31 +990,6 @@
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"src/app/(dashboard)/dashboard/memory/components/EditMemoryModal.tsx": {
|
||||
"react-hooks/set-state-in-effect": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/app/(dashboard)/dashboard/memory/components/QdrantConfigCard.tsx": {
|
||||
"react-hooks/set-state-in-effect": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/app/(dashboard)/dashboard/memory/components/tabs/MemoriesTab.tsx": {
|
||||
"react-hooks/set-state-in-effect": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/app/(dashboard)/dashboard/memory/hooks/useEngineStatus.ts": {
|
||||
"react-hooks/set-state-in-effect": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/app/(dashboard)/dashboard/memory/hooks/useMemorySettings.ts": {
|
||||
"react-hooks/set-state-in-effect": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/app/(dashboard)/dashboard/onboarding/page.tsx": {
|
||||
"@typescript-eslint/no-unused-vars": {
|
||||
"count": 1
|
||||
@@ -1185,29 +1090,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/app/(dashboard)/dashboard/radar/RadarCatalogTable.tsx": {
|
||||
"react-hooks/set-state-in-effect": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/app/(dashboard)/dashboard/radar/intel/page.tsx": {
|
||||
"react-hooks/set-state-in-effect": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/app/(dashboard)/dashboard/radar/page.tsx": {
|
||||
"react-hooks/set-state-in-effect": {
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"src/app/(dashboard)/dashboard/radar/setup/page.tsx": {
|
||||
"react-hooks/preserve-manual-memoization": {
|
||||
"count": 1
|
||||
},
|
||||
"react-hooks/set-state-in-effect": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/app/(dashboard)/dashboard/relay/RelayProxyClient.tsx": {
|
||||
"react-hooks/set-state-in-effect": {
|
||||
"count": 1
|
||||
@@ -1376,11 +1258,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/app/(dashboard)/dashboard/usage/components/EvalsTab.tsx": {
|
||||
"react-hooks/set-state-in-effect": {
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"src/app/(dashboard)/dashboard/usage/components/ProviderLimits/ProviderLimitCard.tsx": {
|
||||
"@typescript-eslint/no-unused-vars": {
|
||||
"count": 3
|
||||
@@ -1391,11 +1268,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/app/(dashboard)/dashboard/usage/components/ProviderLimits/useCodexResetCreditRedemption.ts": {
|
||||
"react-hooks/immutability": {
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"src/app/(dashboard)/dashboard/usage/components/ProviderLimits/utils.tsx": {
|
||||
"@typescript-eslint/no-unused-vars": {
|
||||
"count": 1
|
||||
@@ -1404,17 +1276,11 @@
|
||||
"src/app/(dashboard)/dashboard/usage/components/RateLimitStatus.tsx": {
|
||||
"@typescript-eslint/no-unused-vars": {
|
||||
"count": 1
|
||||
},
|
||||
"react-hooks/set-state-in-effect": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/app/(dashboard)/dashboard/usage/components/SessionsTab.tsx": {
|
||||
"@typescript-eslint/no-unused-vars": {
|
||||
"count": 1
|
||||
},
|
||||
"react-hooks/set-state-in-effect": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/app/(dashboard)/dashboard/webhooks/WebhooksPageClient.tsx": {
|
||||
@@ -1437,17 +1303,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/app/(dashboard)/home/ProviderQuotaWidget.tsx": {
|
||||
"react-hooks/purity": {
|
||||
"count": 1
|
||||
},
|
||||
"react-hooks/refs": {
|
||||
"count": 1
|
||||
},
|
||||
"react-hooks/set-state-in-effect": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/app/api/assess/route.ts": {
|
||||
"@typescript-eslint/no-unused-vars": {
|
||||
"count": 1
|
||||
@@ -2298,9 +2153,6 @@
|
||||
"src/shared/components/RequestLoggerV2.tsx": {
|
||||
"@typescript-eslint/no-unused-vars": {
|
||||
"count": 3
|
||||
},
|
||||
"react-hooks/exhaustive-deps": {
|
||||
"count": 6
|
||||
}
|
||||
},
|
||||
"src/shared/components/RequestTimeline.tsx": {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
{
|
||||
"_rebaseline_2026_08_20_10531_freebuff_provider": "PR #10531 (adrianaryaputra, feat/freebuff-provider-support, closes #6793) own growth: src/shared/constants/providers/apikey/gateways.ts 1283->1298 (+15, the freebuff APIKEY_PROVIDERS_GATEWAYS catalog entry, additive data at the existing registry chokepoint, same god-file no-split rationale as prior gateways.ts rebaselines) and src/app/(dashboard)/dashboard/providers/[id]/components/modals/AddApiKeyModal.tsx 1062->1067 (+5, freebuff credential placeholder/hint at the existing per-provider switch chokepoint). Covered by tests/unit/freebuff-provider.test.ts (9/9 passing).",
|
||||
"_rebaseline_2026_08_31_12212_openapi_generated": "PR #12212 (docs audit follow-up nº 3): src/app/docs/lib/openapi.generated.ts 171->1347 — the module is emitted by scripts/docs/gen-openapi-module.mjs from docs/openapi.yaml, and the spec now documents all 692 implemented routes (was 276), so the generated output grew with the spec. Frozen at the generator output size; shrink by slimming the spec, never by hand-editing the generated module. Covered by tests/unit/openapi-security-tiers.test.ts (6/6) and the check:api-docs-refs gate (692/692 paths with a real route).",
|
||||
"_rebaseline_2026_08_21_10987_logfare_provider": "PR #10987 (jonlwheat2-gif, feat/10644-logfare-provider, closes #10644) own growth: src/shared/constants/providers/apikey/gateways.ts 1298->1321 (+23, the logfare APIKEY_PROVIDERS_GATEWAYS catalog entry with Free badge/freeNote/apiHint documenting the request-logging policy, additive data at the existing registry chokepoint, same god-file no-split rationale as the prior gateways.ts rebaselines: #10531 freebuff, merge-storm 2026-08-11). Covered by tests/unit/logfare-registry.test.ts (1/1 passing).",
|
||||
"_rebaseline_2026_08_20_10574_reasoning_transport_fallback": "PR #10574 (jackjinke, fix/responses-reasoning-transport, fixes #10550) own growth: src/sse/handlers/chatHelpers.ts 1017->1019 (+2 = the new reasoningTransportFallback option threaded through executeChatWithBreaker's options destructure and its downstream handleSingleModel call, at the existing per-attempt options-passthrough chokepoint; not extractable without splitting the option-forwarding call itself). Covered by the PR's own reasoning-policy test suite (tests/unit/chatcore-translation-paths.test.ts, tests/unit/combo-attempt-body-isolation-7847.test.ts, tests/unit/reasoning-cache.test.ts, tests/unit/strip-reasoning-blobs-agentic-context-1599.test.ts among others), 446/446 focused tests passing.",
|
||||
"_rebaseline_2026_08_18_10517_zed_hosted_oauth_callback_port": "PR #10517 (phatchau036, fix/zed-hosted-oauth-callback-port) own growth: src/shared/components/OAuthModal.tsx 1131->1148 (wc -l; check-file-size.mjs counts via split(\"\\n\").length so the gate sees 1134->1149, +15/+18, crosses the frozen 1134 cap). Wires the zed-hosted native-app callback auto-complete: forceManual gating on isTrueLocalhost for zed-hosted, the loopback-redirect-URI comment block, and the exchangeToken full-URL-as-code branch, all at the existing provider-switch chokepoints this modal already carries growth for (seventh bump: 969->989->993->998->1030->1056->1100->1149; structural shrink tracked in #3501). The actual port-derivation logic lives in src/lib/oauth/providers/zed-hosted.ts (not frozen here) and was hardened during pre-merge review to use the server's own getRuntimePorts() instead of a browser-guessed scheme/port, covered by the new tests/unit/zed-hosted-loopback-port-derivation.test.ts (8/8 passing).",
|
||||
@@ -496,8 +495,7 @@
|
||||
"src/shared/components/ModelSelectModal.tsx": 1138,
|
||||
"src/shared/constants/providers/apikey/gateways.ts": 1250
|
||||
},
|
||||
"open-sse/executors/commandCode.ts": 1271,
|
||||
"src/app/docs/lib/openapi.generated.ts": 1347
|
||||
"open-sse/executors/commandCode.ts": 1271
|
||||
},
|
||||
"_rebaseline_base_2026_08_10_proxyfetch": "Base-red fix (green-prs sweep, issue #9985): open-sse/utils/proxyFetch.ts 1207 > cap 1000 — new proxied-TLS fetch helper introduced by the Fal reference-image work. Owner-authorized quick rebaseline to green; structural slim tracked for v3.9.0.",
|
||||
"_rebaseline_2026_07_27_v3849_train2": "Merge-train 2 (7 PRs) — owner-approved 2026-07-27. Single entry: chatCore.ts 4955->5006 (#8595, Responses multi-turn image compaction before the context hard-reject). Genuine irreducible growth at the existing compaction chokepoint in handleChatCore — the PR adds a last-resort retry against the concrete budget plus the estimateFinalInputTokens helper, both wired at the pre-existing call site rather than a new branch. Covered by tests/unit/8560-responses-image-compaction.test.ts (4 tests).",
|
||||
|
||||
@@ -17,7 +17,7 @@ It provides a single OpenAI-compatible endpoint (`/v1/*`) and routes traffic acr
|
||||
|
||||
Core capabilities:
|
||||
|
||||
- OpenAI-compatible API surface for CLI/tools (352 providers, 104 executors)
|
||||
- OpenAI-compatible API surface for CLI/tools (352 providers, 106 executors)
|
||||
- Request/response translation across provider formats
|
||||
- Model combo fallback (multi-model sequence)
|
||||
- Structured combo steps (`provider + model + connection`) with runtime ordering by `compositeTiers`
|
||||
|
||||
@@ -450,7 +450,7 @@ open-sse/
|
||||
├── types.d.ts
|
||||
├── config/ Provider registries, header profiles, identity, …
|
||||
├── handlers/ Request handlers (chat, embeddings, audio, image, …)
|
||||
├── executors/ 104 provider-specific HTTP executors
|
||||
├── executors/ 106 provider-specific HTTP executors
|
||||
├── translator/ Format conversion (OpenAI ↔ Claude ↔ Gemini ↔ Cursor ↔ Kiro)
|
||||
├── transformer/ Responses API ↔ Chat Completions stream transformer
|
||||
├── services/ 80+ service modules (combos, fallback, quotas, identity, …)
|
||||
@@ -480,7 +480,7 @@ open-sse/
|
||||
|
||||
### 4.2 `open-sse/executors/`
|
||||
|
||||
104 provider executors, each extending `BaseExecutor` (`base.ts`):
|
||||
106 provider executors, each extending `BaseExecutor` (`base.ts`):
|
||||
|
||||
`antigravity`, `azure-openai`, `blackbox-web`, `cliproxyapi`,
|
||||
`chatgpt-web-codex`, `cloudflare-ai`, `codex`, `commandCode`, `cursor`, `default`, `devin-cli`,
|
||||
|
||||
@@ -50,6 +50,25 @@ OmniRoute has three distinct but related resilience mechanisms. Each has a diffe
|
||||
|
||||
---
|
||||
|
||||
### Opt-in global Provider Cooldown (window gate)
|
||||
|
||||
A fourth, **opt-in** layer (`PROVIDER_COOLDOWN_ENABLED`, default **off**) keeps a
|
||||
cross-request memory of failing providers in
|
||||
`open-sse/services/providerCooldownTracker.ts`, consulted by combo target
|
||||
resolution so consecutive combo requests stop re-walking a provider that just
|
||||
failed. Provider-level entries honor the `PROVIDER_PROFILES` window gate:
|
||||
|
||||
| Profile | trips after (`providerFailureThreshold`) | inside (`providerFailureWindowMs`) | cools for (`providerCooldownMs`) |
|
||||
| ------- | ---------------------------------------: | ---------------------------------: | -------------------------------: |
|
||||
| OAuth | `10` | `15min` | `5min` |
|
||||
| API key | `15` | `30min` | `10min` |
|
||||
|
||||
Below the threshold the provider is **not** considered cooling; a success clears
|
||||
the window. Connection-level entries (`provider:connectionId`) keep the
|
||||
exponential `minRetryCooldownMs → maxRetryCooldownMs` backoff instead. Overrides:
|
||||
`OMNIROUTE_PROVIDER_BREAKER_{OAUTH,API_KEY}_{FAILURE_THRESHOLD,FAILURE_WINDOW_MS,COOLDOWN_MS}`.
|
||||
Regression guard: `tests/unit/provider-cooldown-window-gate.test.ts`.
|
||||
|
||||
## 2. Connection Cooldown
|
||||
|
||||
**Scope:** single provider connection/account/key.
|
||||
|
||||
4945
docs/openapi.yaml
4945
docs/openapi.yaml
File diff suppressed because it is too large
Load Diff
@@ -414,6 +414,8 @@ Persisted `strategy: "auto"` combos can set `config.routerStrategy` (or legacy
|
||||
`config.auto.routerStrategy`) to one of:
|
||||
|
||||
- `rules` — default weighted scoring
|
||||
- `score` — selects the highest configured weighted score. Exact ties preserve configured
|
||||
candidate order; the existing `explorationRate` samples from the full ranked pool.
|
||||
- `cost` / `eco` — cheapest healthy provider
|
||||
- `latency` / `fast` — lowest p95 latency with reliability penalty
|
||||
- `sla-aware` / `sla` — prefer candidates that satisfy p95 latency, error-rate, and optional
|
||||
@@ -422,7 +424,7 @@ Persisted `strategy: "auto"` combos can set `config.routerStrategy` (or legacy
|
||||
|
||||
### Router strategies in detail
|
||||
|
||||
The auto-combo engine exposes 5 pluggable **RouterStrategy** implementations that
|
||||
The auto-combo engine exposes 6 pluggable **RouterStrategy** implementations that
|
||||
you can swap via `config.routerStrategy` (or the legacy `config.auto.routerStrategy`).
|
||||
Each strategy picks one provider from the candidate pool, given a `RoutingContext`
|
||||
(task type, tool/vision hints, token estimate, optional SLA policy, optional
|
||||
|
||||
@@ -570,3 +570,37 @@ export function isVerifiedNativeCodexRequest(
|
||||
): boolean {
|
||||
return isCodexOriginatedHeaders(headers) && hasNativeCodexTurnBinding(body);
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect the Claude Code CLI as the request *client* from request headers.
|
||||
* Used to auto-enable model echo so session restores work when the resolved
|
||||
* upstream model (e.g. `oc/nemotron-3-ultra-free`) is not recognized by the
|
||||
* Claude Code client on `--resume`.
|
||||
*/
|
||||
export function isClaudeCodeOriginatedHeaders(
|
||||
headers: Headers | Record<string, unknown> | null | undefined
|
||||
): boolean {
|
||||
const getHeader = (name: string): string => {
|
||||
if (headers instanceof Headers) {
|
||||
return headers.get(name)?.toLowerCase() ?? "";
|
||||
}
|
||||
if (headers && typeof headers === "object") {
|
||||
for (const [key, value] of Object.entries(headers as Record<string, unknown>)) {
|
||||
if (key.toLowerCase() === name && typeof value === "string") {
|
||||
return value.toLowerCase();
|
||||
}
|
||||
}
|
||||
}
|
||||
return "";
|
||||
};
|
||||
|
||||
// Claude Code identifies itself via the user-agent header
|
||||
const userAgent = getHeader("user-agent");
|
||||
if (userAgent.includes("claude-code") || userAgent.includes("anthropic-ai/claude-code")) {
|
||||
return true;
|
||||
}
|
||||
// Also check originator if present
|
||||
const originator = getHeader("originator");
|
||||
if (originator.startsWith("claude-code")) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -239,7 +239,7 @@ export const EMBEDDING_PROVIDERS: Record<string, EmbeddingProvider> = {
|
||||
{
|
||||
id: "google/gemini-embedding-001",
|
||||
name: "Gemini Embedding 001 (OpenRouter)",
|
||||
dimensions: 768,
|
||||
dimensions: 3072,
|
||||
},
|
||||
{
|
||||
id: "google/gemini-embedding-2",
|
||||
|
||||
54
open-sse/executors/credential.ts
Normal file
54
open-sse/executors/credential.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { assertCommonChatGptWebProviderAvailable } from "@/shared/constants/chatgptWebRetirement";
|
||||
import { assertMicrosoftDesignerWebProviderAvailable } from "@/shared/constants/designerWebRetirement";
|
||||
import { assertRuntimeProviderAvailable } from "@/shared/constants/providerRetirement";
|
||||
import type { BaseExecutor } from "./base.ts";
|
||||
import { getDefaultExecutor } from "./defaultResolver.ts";
|
||||
|
||||
type CredentialExecutorLoader = () => Promise<BaseExecutor>;
|
||||
|
||||
const specializedCredentialExecutors: Record<string, CredentialExecutorLoader> = {
|
||||
antigravity: () => import("./antigravity.ts").then((m) => new m.AntigravityExecutor()),
|
||||
agy: () => import("./antigravity.ts").then((m) => new m.AntigravityExecutor()),
|
||||
github: () => import("./github.ts").then((m) => new m.GithubExecutor()),
|
||||
"ghe-copilot": () => import("./ghe-copilot.ts").then((m) => new m.GheCopilotExecutor()),
|
||||
kiro: () => import("./kiro.ts").then((m) => new m.KiroExecutor()),
|
||||
"amazon-q": () => import("./kiro.ts").then((m) => new m.KiroExecutor("amazon-q")),
|
||||
codex: () => import("./codex.ts").then((m) => new m.CodexExecutor()),
|
||||
cursor: () => import("./cursor.ts").then((m) => new m.CursorExecutor()),
|
||||
cu: () => import("./cursor.ts").then((m) => new m.CursorExecutor()),
|
||||
"cursor-api": () => import("./cursor.ts").then((m) => new m.CursorExecutor("cursor-api")),
|
||||
cua: () => import("./cursor.ts").then((m) => new m.CursorExecutor("cursor-api")),
|
||||
trae: () => import("./trae.ts").then((m) => new m.TraeExecutor()),
|
||||
gitlab: () => import("./gitlab.ts").then((m) => new m.GitlabExecutor()),
|
||||
"gitlab-duo": () => import("./gitlab.ts").then((m) => new m.GitlabExecutor("gitlab-duo")),
|
||||
"zed-hosted": () => import("./zed-hosted.ts").then((m) => new m.ZedHostedExecutor()),
|
||||
"grok-cli": () => import("./grok-cli.ts").then((m) => new m.GrokCliExecutor()),
|
||||
gc: () => import("./grok-cli.ts").then((m) => new m.GrokCliExecutor()),
|
||||
auggie: () => import("./auggie.ts").then((m) => new m.AuggieExecutor()),
|
||||
xai: () => import("./xai.ts").then((m) => new m.XaiExecutor()),
|
||||
"xai-oauth": () => import("./xai.ts").then((m) => new m.XaiExecutor("xai-oauth")),
|
||||
xao: () => import("./xai.ts").then((m) => new m.XaiExecutor("xai-oauth")),
|
||||
};
|
||||
|
||||
const credentialExecutorCache = new Map<string, Promise<BaseExecutor>>();
|
||||
|
||||
/** Resolve only executors with credential-refresh behavior, without loading the chat registry. */
|
||||
export async function getCredentialRefreshExecutor(provider: string): Promise<BaseExecutor> {
|
||||
assertMicrosoftDesignerWebProviderAvailable(provider);
|
||||
assertRuntimeProviderAvailable(provider);
|
||||
assertCommonChatGptWebProviderAvailable(provider);
|
||||
|
||||
let executor = credentialExecutorCache.get(provider);
|
||||
if (!executor) {
|
||||
const specializedLoader = specializedCredentialExecutors[provider];
|
||||
executor = specializedLoader
|
||||
? specializedLoader()
|
||||
: Promise.resolve(getDefaultExecutor(provider));
|
||||
executor = executor.catch((error) => {
|
||||
credentialExecutorCache.delete(provider);
|
||||
throw error;
|
||||
});
|
||||
credentialExecutorCache.set(provider, executor);
|
||||
}
|
||||
return executor;
|
||||
}
|
||||
13
open-sse/executors/defaultResolver.ts
Normal file
13
open-sse/executors/defaultResolver.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { DefaultExecutor } from "./default.ts";
|
||||
|
||||
const defaultExecutorCache = new Map<string, DefaultExecutor>();
|
||||
|
||||
/** Resolve the shared fallback executor without initializing the specialized executor registry. */
|
||||
export function getDefaultExecutor(provider: string): DefaultExecutor {
|
||||
let executor = defaultExecutorCache.get(provider);
|
||||
if (!executor) {
|
||||
executor = new DefaultExecutor(provider);
|
||||
defaultExecutorCache.set(provider, executor);
|
||||
}
|
||||
return executor;
|
||||
}
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
} from "./registry.ts";
|
||||
// Type-only: pulls no runtime code, keeps DefaultExecutor the only eager class.
|
||||
import type { BaseExecutor } from "./base.ts";
|
||||
import { DefaultExecutor } from "./default.ts";
|
||||
import { getDefaultExecutor } from "./defaultResolver.ts";
|
||||
|
||||
// R0.3 — declarative built-in table, made LAZY by #11220.
|
||||
//
|
||||
@@ -207,8 +207,6 @@ for (const [alias, load] of Object.entries(lazyExecutors)) {
|
||||
registerLazyExecutor(alias, load);
|
||||
}
|
||||
|
||||
const defaultCache = new Map();
|
||||
|
||||
// #6699 — providers that exist ONLY as Cloud Agent task-API entries
|
||||
// (CLOUD_AGENT_PROVIDERS / staticModels "Available Models" catalog) and have no
|
||||
// chat-completions REGISTRY entry anywhere in open-sse/. Without this guard,
|
||||
@@ -251,8 +249,7 @@ export async function getExecutor(provider: string): Promise<BaseExecutor> {
|
||||
(err as Error & { status?: number }).status = 400;
|
||||
throw err;
|
||||
}
|
||||
if (!defaultCache.has(provider)) defaultCache.set(provider, new DefaultExecutor(provider));
|
||||
return defaultCache.get(provider)!;
|
||||
return getDefaultExecutor(provider);
|
||||
}
|
||||
|
||||
export function hasSpecializedExecutor(provider: string): boolean {
|
||||
|
||||
@@ -77,7 +77,7 @@ import {
|
||||
isStripReasoningRequested,
|
||||
} from "./chatCore/headers.ts";
|
||||
import { markCodexScopeRateLimited } from "./chatCore/codexFailover.ts";
|
||||
import { getCodexClientSessionId, isCodexOriginatedHeaders } from "../config/codexIdentity.ts";
|
||||
import { getCodexClientSessionId, isCodexOriginatedHeaders, isClaudeCodeOriginatedHeaders } from "../config/codexIdentity.ts";
|
||||
import {
|
||||
noteCodexTurnStateProvenance,
|
||||
readCodexTurnStateHeader,
|
||||
@@ -981,8 +981,14 @@ export async function handleChatCore({
|
||||
const isCodexResponsesEcho =
|
||||
(isResponsesEndpoint || sourceFormat === FORMATS.OPENAI_RESPONSES) &&
|
||||
isCodexOriginatedHeaders(clientRawRequest?.headers);
|
||||
|
||||
// Detect Claude Code CLI so we can auto-enable model echo — this prevents
|
||||
// session restore failures when the resolved upstream model (e.g.
|
||||
// `oc/nemotron-3-ultra-free`) is not recognized by the client on `--resume`.
|
||||
const isClaudeCodeClient = isClaudeCodeOriginatedHeaders(clientRawRequest?.headers);
|
||||
|
||||
let echoModel =
|
||||
(settings.echoRequestedModelName === true || isCodexResponsesEcho) &&
|
||||
(settings.echoRequestedModelName === true || isCodexResponsesEcho || isClaudeCodeClient) &&
|
||||
typeof requestedModel === "string" &&
|
||||
requestedModel
|
||||
? requestedModel
|
||||
@@ -5465,6 +5471,7 @@ export async function handleChatCore({
|
||||
|
||||
const streamReadiness = await ensureStreamReadiness(providerResponse, {
|
||||
timeoutMs: streamReadinessPolicy.timeoutMs,
|
||||
maxTimeoutMs: streamReadinessPolicy.maxTimeoutMs,
|
||||
provider,
|
||||
model,
|
||||
log,
|
||||
|
||||
@@ -4,13 +4,14 @@
|
||||
* Inspired by ClawRouter commit 14c83c258 "refactor: extract routing into pluggable RouterStrategy system".
|
||||
* Provides a RouterStrategy interface and built-in implementations:
|
||||
* - RulesStrategy (default): wraps the existing 15-factor scoring engine
|
||||
* - ScoreStrategy: highest configured weighted score, with explicit exploration
|
||||
* - CostStrategy: always picks cheapest available model
|
||||
* - LatencyStrategy: prioritizes low p95 latency with reliability weighting
|
||||
* - SLAStrategy: prefers candidates that satisfy latency/error/cost SLOs
|
||||
* - LKGPStrategy: tries last known good provider first
|
||||
*/
|
||||
|
||||
import type { ProviderCandidate, ScoredProvider } from "./scoring.ts";
|
||||
import type { ProviderCandidate, ScoredProvider, ScoringWeights } from "./scoring.ts";
|
||||
import { scorePool } from "./scoring.ts";
|
||||
import { getTaskFitness } from "./taskFitness.ts";
|
||||
import { clamp01 } from "../../utils/number.ts";
|
||||
@@ -32,6 +33,8 @@ export interface RoutingContext {
|
||||
lastKnownGoodProvider?: string;
|
||||
lkgpEnabled?: boolean;
|
||||
sla?: SlaRoutingPolicy;
|
||||
weights?: ScoringWeights;
|
||||
explorationRate?: number;
|
||||
}
|
||||
|
||||
export interface RoutingDecision {
|
||||
@@ -108,6 +111,38 @@ class RulesStrategyImpl implements RouterStrategy {
|
||||
}
|
||||
}
|
||||
|
||||
// ── ScoreStrategy: configured score wins, with explicit exploration ──────────
|
||||
|
||||
class ScoreStrategyImpl implements RouterStrategy {
|
||||
readonly name = "score";
|
||||
readonly description = "Selects the highest configured weighted score, with explicit exploration";
|
||||
|
||||
select(pool: ProviderCandidate[], context: RoutingContext): RoutingDecision {
|
||||
const eligible = pool.filter((candidate) => candidate.circuitBreakerState !== "OPEN");
|
||||
const ranked = scorePool(
|
||||
eligible.length > 0 ? eligible : pool,
|
||||
context.taskType,
|
||||
context.weights,
|
||||
getTaskFitness
|
||||
);
|
||||
if (ranked.length === 0) throw new Error("[ScoreStrategy] No candidates to score");
|
||||
|
||||
const explorationRate = Math.min(1, Math.max(0, context.explorationRate ?? 0));
|
||||
const isExploration = Math.random() < explorationRate && ranked.length > 1;
|
||||
const selected = isExploration ? ranked[Math.floor(Math.random() * ranked.length)] : ranked[0];
|
||||
|
||||
return {
|
||||
provider: selected.provider,
|
||||
model: selected.model,
|
||||
strategy: this.name,
|
||||
reason: `ScoreStrategy: score=${selected.score.toFixed(3)}${isExploration ? " (exploration)" : ""}`,
|
||||
candidatesConsidered: ranked.length,
|
||||
finalScore: selected.score,
|
||||
connectionId: selected.connectionId,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// ── CostStrategy: always picks cheapest healthy provider ─────────────────────
|
||||
|
||||
class CostStrategyImpl implements RouterStrategy {
|
||||
@@ -337,12 +372,14 @@ class LKGPStrategyImpl implements RouterStrategy {
|
||||
const strategyRegistry = new Map<string, RouterStrategy>();
|
||||
|
||||
const rulesStrategy = new RulesStrategyImpl();
|
||||
const scoreStrategy = new ScoreStrategyImpl();
|
||||
const costStrategy = new CostStrategyImpl();
|
||||
const latencyStrategy = new LatencyStrategyImpl();
|
||||
const slaStrategy = new SLAStrategyImpl();
|
||||
const lkgpStrategy = new LKGPStrategyImpl();
|
||||
|
||||
strategyRegistry.set("rules", rulesStrategy);
|
||||
strategyRegistry.set("score", scoreStrategy);
|
||||
strategyRegistry.set("cost", costStrategy);
|
||||
strategyRegistry.set("eco", costStrategy); // alias
|
||||
strategyRegistry.set("latency", latencyStrategy);
|
||||
|
||||
@@ -377,6 +377,8 @@ export async function resolveAutoStrategyOrder(
|
||||
boolean | undefined,
|
||||
estimatedInputTokens,
|
||||
sla: slaPolicy,
|
||||
weights,
|
||||
explorationRate,
|
||||
},
|
||||
routingStrategy
|
||||
);
|
||||
|
||||
@@ -11,6 +11,8 @@ import {
|
||||
DEFAULT_RESILIENCE_SETTINGS,
|
||||
type ResilienceSettings,
|
||||
} from "../../src/lib/resilience/settings";
|
||||
import { PROVIDER_PROFILES } from "../config/constants.ts";
|
||||
import { getProviderCategory } from "../config/providerRegistry.ts";
|
||||
|
||||
interface CooldownEntry {
|
||||
/** Timestamp of last recorded failure (ms since epoch) */
|
||||
@@ -19,6 +21,47 @@ interface CooldownEntry {
|
||||
failureCount: number;
|
||||
/** How long this entry must be retained for cleanup purposes */
|
||||
retentionMs: number;
|
||||
/**
|
||||
* Provider-level entries only: timestamps of recent failures, pruned to the
|
||||
* profile's `providerFailureWindowMs`. Powers the PROVIDER_PROFILES window
|
||||
* gate (`providerFailureThreshold` failures inside the window trip a
|
||||
* `providerCooldownMs` cooldown for the whole provider).
|
||||
*/
|
||||
failureTimestamps?: number[];
|
||||
}
|
||||
|
||||
// ── PROVIDER_PROFILES window gate (whole-provider scope) ─────────────────────
|
||||
// `providerFailureThreshold` / `providerFailureWindowMs` / `providerCooldownMs`
|
||||
// shipped in PROVIDER_PROFILES with no runtime consumer (2026-08-31 docs
|
||||
// audit, P0.1). Provider-level entries (no connectionId) now honor them: the
|
||||
// provider only counts as cooling after `providerFailureThreshold` failures
|
||||
// inside `providerFailureWindowMs`, and then cools for `providerCooldownMs`.
|
||||
// Connection-level entries keep the pre-existing exponential backoff.
|
||||
function providerWindowProfile(provider: string) {
|
||||
const category = getProviderCategory(provider);
|
||||
const profile = PROVIDER_PROFILES[category] ?? PROVIDER_PROFILES.apikey;
|
||||
return {
|
||||
failureThreshold: profile.providerFailureThreshold,
|
||||
failureWindowMs: profile.providerFailureWindowMs,
|
||||
cooldownMs: profile.providerCooldownMs,
|
||||
};
|
||||
}
|
||||
|
||||
function pruneWindow(timestamps: number[], windowMs: number, now: number): number[] {
|
||||
const cutoff = now - windowMs;
|
||||
const pruned = timestamps.filter((t) => t >= cutoff);
|
||||
// Memory bound: the gate only ever needs `failureThreshold` recent samples;
|
||||
// keep a small multiple so bursts cannot grow the array unbounded.
|
||||
return pruned.length > 200 ? pruned.slice(-200) : pruned;
|
||||
}
|
||||
|
||||
function providerWindowCooldownMs(provider: string, entry: CooldownEntry, now: number): number {
|
||||
const { failureThreshold, failureWindowMs, cooldownMs } = providerWindowProfile(provider);
|
||||
const inWindow = pruneWindow(entry.failureTimestamps ?? [], failureWindowMs, now);
|
||||
if (inWindow.length < failureThreshold) return 0;
|
||||
const elapsed = now - entry.lastFailureAt;
|
||||
const remaining = cooldownMs - elapsed;
|
||||
return remaining > 0 ? remaining : 0;
|
||||
}
|
||||
|
||||
// Global cooldown state: keyed by "provider:connectionId" or "provider"
|
||||
@@ -90,8 +133,21 @@ export function recordProviderCooldown(
|
||||
existing.lastFailureAt = now;
|
||||
existing.failureCount++;
|
||||
existing.retentionMs = Math.max(existing.retentionMs, retentionMs);
|
||||
if (!connectionId) {
|
||||
const { failureWindowMs } = providerWindowProfile(provider);
|
||||
existing.failureTimestamps = pruneWindow(
|
||||
[...(existing.failureTimestamps ?? []), now],
|
||||
failureWindowMs,
|
||||
now
|
||||
);
|
||||
}
|
||||
} else {
|
||||
cooldownMap.set(key, { lastFailureAt: now, failureCount: 1, retentionMs });
|
||||
cooldownMap.set(key, {
|
||||
lastFailureAt: now,
|
||||
failureCount: 1,
|
||||
retentionMs,
|
||||
...(connectionId ? {} : { failureTimestamps: [now] }),
|
||||
});
|
||||
}
|
||||
|
||||
startCleanupIfNeeded();
|
||||
@@ -119,6 +175,11 @@ export function isProviderInCooldown(
|
||||
if (entry.failureCount === 0) return false;
|
||||
|
||||
const now = Date.now();
|
||||
|
||||
if (!connectionId) {
|
||||
return providerWindowCooldownMs(provider, entry, now) > 0;
|
||||
}
|
||||
|
||||
const elapsed = now - entry.lastFailureAt;
|
||||
|
||||
const minCooldownMs =
|
||||
@@ -151,6 +212,12 @@ export function getRemainingCooldownMs(
|
||||
if (!entry) return 0;
|
||||
|
||||
const now = Date.now();
|
||||
|
||||
if (!connectionId) {
|
||||
if (entry.failureCount === 0) return 0;
|
||||
return providerWindowCooldownMs(provider, entry, now);
|
||||
}
|
||||
|
||||
const elapsed = now - entry.lastFailureAt;
|
||||
|
||||
const minCooldownMs =
|
||||
@@ -183,8 +250,9 @@ export function recordProviderSuccess(provider: string, connectionId: string | u
|
||||
const key = cooldownKey(provider, connectionId);
|
||||
const entry = cooldownMap.get(key);
|
||||
if (entry) {
|
||||
// Reset failure count but keep the entry
|
||||
// Reset failure count and the provider-level failure window, keep the entry
|
||||
entry.failureCount = 0;
|
||||
entry.failureTimestamps = [];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -43,7 +43,19 @@ export function resolveReasoningTransport(
|
||||
): ReasoningTransport {
|
||||
const normalized = typeof provider === "string" ? provider.trim().toLowerCase() : "";
|
||||
const transport = REASONING_TRANSPORTS.get(normalized);
|
||||
return transport ?? (preserveEncryptedReasoning ? "opaque" : "plaintext");
|
||||
if (transport) return transport;
|
||||
// #12128: Generic Responses-protocol endpoints (e.g. openai-compatible-responses-*,
|
||||
// custom-openai-responses, proxy backends) implement the OpenAI/Codex Responses API
|
||||
// where reasoning input items cannot accept plaintext content (maxItems: 0).
|
||||
if (
|
||||
normalized.startsWith("openai-compatible-responses") ||
|
||||
normalized.startsWith("custom-openai-responses") ||
|
||||
normalized.includes("codex") ||
|
||||
normalized.includes("responses")
|
||||
) {
|
||||
return "opaque";
|
||||
}
|
||||
return preserveEncryptedReasoning ? "opaque" : "plaintext";
|
||||
}
|
||||
|
||||
function asRecord(value: unknown): JsonRecord | null {
|
||||
|
||||
@@ -40,6 +40,7 @@ import {
|
||||
normalizeResponsesReasoningEffort,
|
||||
RESPONSES_STORE_MARKER,
|
||||
} from "./request/openai-responses/helpers.ts";
|
||||
import { applyReasoningInputPolicy } from "../services/reasoningInputPolicy.ts";
|
||||
|
||||
bootstrapTranslatorRegistry();
|
||||
export { register } from "./registry.ts";
|
||||
@@ -575,6 +576,14 @@ export function translateRequest(
|
||||
// Normalize openai-responses input shape for providers that require list input.
|
||||
if (targetFormat === FORMATS.OPENAI_RESPONSES) {
|
||||
result = normalizeOpenAIResponsesRequest(result);
|
||||
// #12128: Sanitize reasoning input items for Responses targets (strip plaintext content for opaque backends)
|
||||
applyReasoningInputPolicy(result as Record<string, unknown>, "responses", {
|
||||
provider,
|
||||
preserveEncryptedReasoning:
|
||||
(credentials as { providerSpecificData?: { preserveEncryptedReasoning?: boolean } } | null)
|
||||
?.providerSpecificData?.preserveEncryptedReasoning === true,
|
||||
onIncompatibleReasoning: "drop",
|
||||
});
|
||||
}
|
||||
|
||||
// Second role normalization: only for OPENAI_RESPONSES. Here messages are built from input
|
||||
|
||||
@@ -220,12 +220,15 @@ function preserveRequired(obj: unknown): void {
|
||||
return;
|
||||
}
|
||||
const record = obj as JsonRecord;
|
||||
if (Array.isArray(record.required) && record.properties && typeof record.properties === "object") {
|
||||
if (
|
||||
Array.isArray(record.required) &&
|
||||
record.properties &&
|
||||
typeof record.properties === "object"
|
||||
) {
|
||||
const properties = record.properties as JsonRecord;
|
||||
const valid = (record.required as unknown[]).filter(
|
||||
(field) =>
|
||||
typeof field === "string" &&
|
||||
Object.prototype.hasOwnProperty.call(properties, field)
|
||||
typeof field === "string" && Object.prototype.hasOwnProperty.call(properties, field)
|
||||
);
|
||||
if (valid.length === 0) {
|
||||
delete record.required;
|
||||
@@ -298,12 +301,13 @@ function convertContent(content) {
|
||||
|
||||
// Function response → collect all, each becomes a separate tool message
|
||||
if (part.functionResponse) {
|
||||
const resp = part.functionResponse.response;
|
||||
const resultPayload =
|
||||
resp && typeof resp === "object" && "result" in resp ? resp.result : (resp ?? {});
|
||||
toolResults.push({
|
||||
role: "tool",
|
||||
tool_call_id: part.functionResponse.id || part.functionResponse.name,
|
||||
content: JSON.stringify(
|
||||
part.functionResponse.response?.result || part.functionResponse.response || {}
|
||||
),
|
||||
content: JSON.stringify(resultPayload),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -316,9 +320,7 @@ function convertContent(content) {
|
||||
const assistantMsg: JsonRecord = { role: "assistant" };
|
||||
if (textParts.length > 0) {
|
||||
assistantMsg.content =
|
||||
textParts.length === 1 && textParts[0].type === "text"
|
||||
? textParts[0].text
|
||||
: textParts;
|
||||
textParts.length === 1 && textParts[0].type === "text" ? textParts[0].text : textParts;
|
||||
}
|
||||
if (reasoningContent) {
|
||||
assistantMsg.reasoning_content = reasoningContent;
|
||||
|
||||
@@ -147,12 +147,13 @@ function convertGeminiContent(content) {
|
||||
}
|
||||
|
||||
if (part.functionResponse) {
|
||||
const resp = part.functionResponse.response;
|
||||
const resultPayload =
|
||||
resp && typeof resp === "object" && "result" in resp ? resp.result : (resp ?? {});
|
||||
return {
|
||||
role: "tool",
|
||||
tool_call_id: part.functionResponse.id || part.functionResponse.name,
|
||||
content: JSON.stringify(
|
||||
part.functionResponse.response?.result || part.functionResponse.response || {}
|
||||
),
|
||||
content: JSON.stringify(resultPayload),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -471,6 +471,9 @@ export async function ensureStreamReadiness(
|
||||
response: Response,
|
||||
options: {
|
||||
timeoutMs: number;
|
||||
/** Hard ceiling for liveness-extended deadlines. When omitted, no hard ceiling
|
||||
* is applied beyond `timeoutMs`. */
|
||||
maxTimeoutMs?: number;
|
||||
provider?: string | null;
|
||||
model?: string | null;
|
||||
log?: StreamReadinessLogger | null;
|
||||
@@ -489,7 +492,14 @@ export async function ensureStreamReadiness(
|
||||
};
|
||||
const startedAt = Date.now();
|
||||
const effectiveTimeoutMs = Math.max(0, Math.floor(options.timeoutMs));
|
||||
const deadline = startedAt + effectiveTimeoutMs;
|
||||
// Hard ceiling: the deadline may extend on liveness signals (bytes arriving),
|
||||
// but never past this absolute maximum. When maxTimeoutMs is omitted the
|
||||
// initial timeoutMs itself acts as the ceiling (no extension).
|
||||
const maxDeadline =
|
||||
options.maxTimeoutMs != null
|
||||
? startedAt + Math.max(effectiveTimeoutMs, Math.floor(options.maxTimeoutMs))
|
||||
: startedAt + effectiveTimeoutMs;
|
||||
let deadline = startedAt + effectiveTimeoutMs;
|
||||
let handedOffReader = false;
|
||||
|
||||
const buildReadyResponse = () =>
|
||||
@@ -500,7 +510,7 @@ export async function ensureStreamReadiness(
|
||||
});
|
||||
|
||||
const timeoutReason = () =>
|
||||
`Stream produced no non-ping SSE event within ${effectiveTimeoutMs}ms`;
|
||||
`Stream produced no non-ping SSE event within ${deadline - startedAt}ms (max=${maxDeadline - startedAt}ms)`;
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
@@ -593,6 +603,22 @@ export async function ensureStreamReadiness(
|
||||
chunks.push(readResult.value);
|
||||
const decodedChunk = decoder.decode(readResult.value, { stream: true });
|
||||
|
||||
// Liveness extension: bytes arrived → connection is alive, not dead.
|
||||
// Reset the deadline so slow-but-alive upstreams (reasoning warm-ups,
|
||||
// keepalive-only phases) are not aborted. The hard ceiling (maxDeadline)
|
||||
// prevents unbounded waits and preserves the operator's fast-fail intent
|
||||
// for truly dead connections.
|
||||
const now = Date.now();
|
||||
if (deadline < maxDeadline) {
|
||||
deadline = Math.min(now + effectiveTimeoutMs, maxDeadline);
|
||||
if (now - startedAt > effectiveTimeoutMs) {
|
||||
options.log?.debug?.(
|
||||
"STREAM",
|
||||
`readiness deadline extended to ${deadline - startedAt}ms (liveness signal) (${options.provider || "provider"}/${options.model || "unknown"})`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (appendStreamReadinessSignal(readinessState, decodedChunk)) {
|
||||
options.log?.debug?.(
|
||||
"STREAM",
|
||||
|
||||
@@ -14,6 +14,7 @@ export type StreamReadinessPolicyInput = {
|
||||
export type StreamReadinessPolicyResult = {
|
||||
timeoutMs: number;
|
||||
baseTimeoutMs: number;
|
||||
maxTimeoutMs: number;
|
||||
reasons: string[];
|
||||
};
|
||||
|
||||
@@ -121,7 +122,7 @@ export function resolveStreamReadinessTimeout(
|
||||
): StreamReadinessPolicyResult {
|
||||
const baseTimeoutMs = Math.max(0, Math.floor(input.baseTimeoutMs || 0));
|
||||
if (baseTimeoutMs <= 0) {
|
||||
return { timeoutMs: baseTimeoutMs, baseTimeoutMs, reasons: ["disabled"] };
|
||||
return { timeoutMs: baseTimeoutMs, baseTimeoutMs, maxTimeoutMs: baseTimeoutMs, reasons: ["disabled"] };
|
||||
}
|
||||
|
||||
const maxTimeoutMs = Math.max(baseTimeoutMs, input.maxTimeoutMs ?? DEFAULT_MAX_TIMEOUT_MS);
|
||||
@@ -197,5 +198,5 @@ export function resolveStreamReadinessTimeout(
|
||||
timeoutMs = Math.min(timeoutMs, maxTimeoutMs);
|
||||
if (timeoutMs === baseTimeoutMs) reasons.push("base");
|
||||
|
||||
return { timeoutMs, baseTimeoutMs, reasons };
|
||||
return { timeoutMs, baseTimeoutMs, maxTimeoutMs, reasons };
|
||||
}
|
||||
|
||||
@@ -1,129 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
// One-shot generator (2026-08-31 docs audit follow-up nº 3): append a minimal,
|
||||
// honest OpenAPI entry for every real route that docs/openapi.yaml does not
|
||||
// document yet. Enumerates routes with the SAME lib the check:api-docs-refs
|
||||
// gate uses, so the generated set can never diverge from the gate's universe.
|
||||
// Minimal by design: real methods (parsed from each route.ts's exports), a
|
||||
// group tag, a neutral path-derived summary and a generic 200 — no invented
|
||||
// semantics. Rich schemas stay hand-curated in the existing entries.
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { collectApiRouteFiles, toApiUrlPath, apiRoot } from "../check/lib/apiRoutes.mjs";
|
||||
import { isLocalOnlyPath, ALWAYS_PROTECTED_API_PATHS } from "../../src/server/authz/routeGuard.ts";
|
||||
|
||||
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
|
||||
const SPEC = path.join(ROOT, "docs", "openapi.yaml");
|
||||
const APPLY = process.argv.includes("--apply");
|
||||
|
||||
const normalizeParams = (p) => p.replace(/\{[^}]+\}/g, "{}");
|
||||
|
||||
// --- real routes + their exported HTTP methods --------------------------------
|
||||
const METHOD_RE =
|
||||
/export\s+(?:async\s+)?function\s+(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\b|export\s+const\s+(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\b|export\s*\{[^}]*\b(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\b[^}]*\}/g;
|
||||
|
||||
function routeMethods(absFile) {
|
||||
const src = fs.readFileSync(absFile, "utf8");
|
||||
const methods = new Set();
|
||||
for (const m of src.matchAll(METHOD_RE)) {
|
||||
const name = m[1] || m[2];
|
||||
if (name) methods.add(name);
|
||||
if (m[3]) {
|
||||
// re-export list: capture every method inside the braces
|
||||
for (const inner of m[0].matchAll(/\b(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\b/g))
|
||||
methods.add(inner[1]);
|
||||
}
|
||||
}
|
||||
methods.delete("OPTIONS"); // CORS preflight — not a documented operation
|
||||
methods.delete("HEAD");
|
||||
return [...methods];
|
||||
}
|
||||
|
||||
const routeFiles = collectApiRouteFiles(ROOT);
|
||||
const API_ROOT = apiRoot(ROOT);
|
||||
const routes = new Map(); // urlPath -> methods
|
||||
for (const rel of routeFiles) {
|
||||
const abs = path.join(ROOT, rel);
|
||||
const url = toApiUrlPath(path.dirname(abs), API_ROOT);
|
||||
if (url) routes.set(url, routeMethods(abs));
|
||||
}
|
||||
|
||||
// --- paths already in the spec -------------------------------------------------
|
||||
const spec = fs.readFileSync(SPEC, "utf8");
|
||||
const specPaths = new Set();
|
||||
for (const m of spec.matchAll(/^ {2}(\/[^\s:]+):\s*$/gm)) specPaths.add(normalizeParams(m[1]));
|
||||
|
||||
const missing = [...routes.entries()]
|
||||
.filter(([url]) => !specPaths.has(normalizeParams(url)))
|
||||
.filter(([, methods]) => methods.length > 0)
|
||||
.sort(([a], [b]) => a.localeCompare(b));
|
||||
|
||||
// --- tag + summary derivation --------------------------------------------------
|
||||
const cap = (s) => s.charAt(0).toUpperCase() + s.slice(1);
|
||||
function groupTag(url) {
|
||||
const seg = url.replace(/^\/api\//, "").split("/");
|
||||
if (seg[0] === "v1") return seg[1] ? `V1 ${cap(seg[1].replace(/\{|\}/g, ""))}` : "V1";
|
||||
return cap(seg[0].replace(/\{|\}/g, "").replace(/-/g, " "));
|
||||
}
|
||||
function summaryFor(url, method) {
|
||||
const tail = url
|
||||
.replace(/^\/api\/(v1\/)?/, "")
|
||||
.replace(/\{([^}]+)\}/g, "<$1>")
|
||||
.replace(/[/]/g, " › ")
|
||||
.replace(/-/g, " ");
|
||||
return `${method} ${tail}`;
|
||||
}
|
||||
|
||||
// --- emit YAML -----------------------------------------------------------------
|
||||
const existingTags = new Set(
|
||||
[...spec.matchAll(/^ {2}- name: (.+)$/gm)].map((m) => m[1].trim().toLowerCase())
|
||||
);
|
||||
const newTags = new Map();
|
||||
const lines = [];
|
||||
lines.push("");
|
||||
lines.push(" # --- Generated route coverage (docs audit 2026-08-31) -----------------------");
|
||||
lines.push(" # Minimal entries for every implemented route not documented above. Methods");
|
||||
lines.push(" # are parsed from each route.ts's exports; summaries are path-derived.");
|
||||
lines.push(
|
||||
" # Regenerate with: node --import tsx/esm scripts/ad-hoc/gen-openapi-missing-paths.mjs --apply"
|
||||
);
|
||||
for (const [url, methods] of missing) {
|
||||
const tag = groupTag(url);
|
||||
if (!existingTags.has(tag.toLowerCase()) && !newTags.has(tag))
|
||||
newTags.set(tag, `${tag} endpoints (generated route coverage)`);
|
||||
lines.push(` ${url}:`);
|
||||
const loopbackOnly = isLocalOnlyPath(url);
|
||||
const alwaysProtected = ALWAYS_PROTECTED_API_PATHS.includes(url);
|
||||
for (const method of methods.sort()) {
|
||||
lines.push(` ${method.toLowerCase()}:`);
|
||||
lines.push(` tags:`);
|
||||
lines.push(` - ${tag}`);
|
||||
lines.push(` summary: "${summaryFor(url, method)}"`);
|
||||
if (loopbackOnly || isLocalOnlyPath(url, method)) lines.push(` x-loopback-only: true`);
|
||||
if (alwaysProtected) lines.push(` x-always-protected: true`);
|
||||
lines.push(` responses:`);
|
||||
lines.push(` "200":`);
|
||||
lines.push(` description: OK`);
|
||||
}
|
||||
}
|
||||
|
||||
const tagLines = [...newTags.entries()]
|
||||
.sort(([a], [b]) => a.localeCompare(b))
|
||||
.map(([name, description]) => ` - name: ${name}\n description: ${description}`)
|
||||
.join("\n");
|
||||
|
||||
console.log(
|
||||
`real routes: ${routes.size} · already in spec: ${specPaths.size} · missing with methods: ${missing.length} · new tags: ${newTags.size}`
|
||||
);
|
||||
if (!APPLY) {
|
||||
console.log("(dry-run) pass --apply to write docs/openapi.yaml");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
let out = spec;
|
||||
// append new tags right after the last existing tag entry (before `paths:`)
|
||||
if (tagLines) out = out.replace(/\npaths:\n/, `\n${tagLines}\n\npaths:\n`);
|
||||
// insert generated paths right before the components section
|
||||
out = out.replace(/\ncomponents:\n/, `\n${lines.join("\n")}\n\ncomponents:\n`);
|
||||
fs.writeFileSync(SPEC, out);
|
||||
console.log(`wrote ${missing.length} paths + ${newTags.size} tags to docs/openapi.yaml`);
|
||||
@@ -137,6 +137,11 @@ function createNextApp() {
|
||||
});
|
||||
}
|
||||
|
||||
// The custom HTTP server owns process exit. Application instrumentation still
|
||||
// registers its cleanup function, but must not install a competing signal
|
||||
// listener that can race this runner's async server/Next teardown.
|
||||
globalThis.__omnirouteCustomServerOwnsShutdown = true;
|
||||
|
||||
let nextApp = createNextApp();
|
||||
|
||||
// Best-effort self-heal for a corrupted Turbopack persistent dev cache (#6289):
|
||||
@@ -231,6 +236,7 @@ async function start() {
|
||||
systemdNotifier.stopping();
|
||||
try {
|
||||
await new Promise((resolve) => server.close(resolve));
|
||||
await globalThis.__omnirouteRequestShutdown?.(signal);
|
||||
await nextApp.close();
|
||||
} catch (error) {
|
||||
console.error("[SHUTDOWN] Failed during signal:", signal, error);
|
||||
|
||||
@@ -74,142 +74,6 @@ curl https://localhost:20128/api/keys/{id}/devices \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### POST /api/keys/{id}/regenerate
|
||||
|
||||
POST keys › <id> › regenerate
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/keys/{id}/regenerate \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### GET /api/keys/{id}/reveal
|
||||
|
||||
GET keys › <id> › reveal
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/keys/{id}/reveal \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### GET /api/keys/{id}/usage-limits
|
||||
|
||||
GET keys › <id> › usage limits
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/keys/{id}/usage-limits \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### GET /api/keys/groups
|
||||
|
||||
GET keys › groups
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/keys/groups \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### POST /api/keys/groups
|
||||
|
||||
POST keys › groups
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/keys/groups \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### GET /api/keys/groups/{id}
|
||||
|
||||
GET keys › groups › <id>
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/keys/groups/{id} \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### PUT /api/keys/groups/{id}
|
||||
|
||||
PUT keys › groups › <id>
|
||||
|
||||
```bash
|
||||
curl -X PUT https://localhost:20128/api/keys/groups/{id} \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### DELETE /api/keys/groups/{id}
|
||||
|
||||
DELETE keys › groups › <id>
|
||||
|
||||
```bash
|
||||
curl -X DELETE https://localhost:20128/api/keys/groups/{id} \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### GET /api/keys/groups/{id}/keys
|
||||
|
||||
GET keys › groups › <id> › keys
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/keys/groups/{id}/keys \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### POST /api/keys/groups/{id}/keys
|
||||
|
||||
POST keys › groups › <id> › keys
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/keys/groups/{id}/keys \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### DELETE /api/keys/groups/{id}/keys
|
||||
|
||||
DELETE keys › groups › <id> › keys
|
||||
|
||||
```bash
|
||||
curl -X DELETE https://localhost:20128/api/keys/groups/{id}/keys \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### GET /api/keys/groups/{id}/permissions
|
||||
|
||||
GET keys › groups › <id> › permissions
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/keys/groups/{id}/permissions \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### POST /api/keys/groups/{id}/permissions
|
||||
|
||||
POST keys › groups › <id> › permissions
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/keys/groups/{id}/permissions \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### DELETE /api/keys/groups/{id}/permissions
|
||||
|
||||
DELETE keys › groups › <id> › permissions
|
||||
|
||||
```bash
|
||||
curl -X DELETE https://localhost:20128/api/keys/groups/{id}/permissions \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
## Payloads
|
||||
|
||||
See the full OpenAPI specification at `GET /api/openapi/spec` or `docs/openapi.yaml` for detailed request/response schemas.
|
||||
|
||||
@@ -69,24 +69,6 @@ curl https://localhost:20128/api/auth/oidc/callback \
|
||||
-b cookie.jar
|
||||
```
|
||||
|
||||
### GET /api/auth/csrf
|
||||
|
||||
GET auth › csrf
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/auth/csrf \
|
||||
-b cookie.jar
|
||||
```
|
||||
|
||||
### GET /api/auth/status
|
||||
|
||||
GET auth › status
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/auth/status \
|
||||
-b cookie.jar
|
||||
```
|
||||
|
||||
## Payloads
|
||||
|
||||
See the full OpenAPI specification at `GET /api/openapi/spec` or `docs/openapi.yaml` for detailed request/response schemas.
|
||||
|
||||
@@ -52,42 +52,6 @@ curl -X DELETE https://localhost:20128/api/cache/stats \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### GET /api/cache/entries
|
||||
|
||||
GET cache › entries
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/cache/entries \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### DELETE /api/cache/entries
|
||||
|
||||
DELETE cache › entries
|
||||
|
||||
```bash
|
||||
curl -X DELETE https://localhost:20128/api/cache/entries \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### GET /api/cache/reasoning
|
||||
|
||||
GET cache › reasoning
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/cache/reasoning \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### DELETE /api/cache/reasoning
|
||||
|
||||
DELETE cache › reasoning
|
||||
|
||||
```bash
|
||||
curl -X DELETE https://localhost:20128/api/cache/reasoning \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
## Payloads
|
||||
|
||||
See the full OpenAPI specification at `GET /api/openapi/spec` or `docs/openapi.yaml` for detailed request/response schemas.
|
||||
|
||||
@@ -385,372 +385,6 @@ curl -X DELETE https://localhost:20128/api/cli-tools/codewhale-settings \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### GET /api/cli-tools/all-statuses
|
||||
|
||||
GET cli tools › all statuses
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/cli-tools/all-statuses \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### POST /api/cli-tools/apply
|
||||
|
||||
POST cli tools › apply
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/cli-tools/apply \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### GET /api/cli-tools/config
|
||||
|
||||
GET cli tools › config
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/cli-tools/config \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### POST /api/cli-tools/config
|
||||
|
||||
POST cli tools › config
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/cli-tools/config \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### GET /api/cli-tools/deepseek-tui-settings
|
||||
|
||||
GET cli tools › deepseek tui settings
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/cli-tools/deepseek-tui-settings \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### POST /api/cli-tools/deepseek-tui-settings
|
||||
|
||||
POST cli tools › deepseek tui settings
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/cli-tools/deepseek-tui-settings \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### DELETE /api/cli-tools/deepseek-tui-settings
|
||||
|
||||
DELETE cli tools › deepseek tui settings
|
||||
|
||||
```bash
|
||||
curl -X DELETE https://localhost:20128/api/cli-tools/deepseek-tui-settings \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### GET /api/cli-tools/detect
|
||||
|
||||
GET cli tools › detect
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/cli-tools/detect \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### GET /api/cli-tools/forge-settings
|
||||
|
||||
GET cli tools › forge settings
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/cli-tools/forge-settings \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### POST /api/cli-tools/forge-settings
|
||||
|
||||
POST cli tools › forge settings
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/cli-tools/forge-settings \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### DELETE /api/cli-tools/forge-settings
|
||||
|
||||
DELETE cli tools › forge settings
|
||||
|
||||
```bash
|
||||
curl -X DELETE https://localhost:20128/api/cli-tools/forge-settings \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### GET /api/cli-tools/grok-build-settings
|
||||
|
||||
GET cli tools › grok build settings
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/cli-tools/grok-build-settings \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### POST /api/cli-tools/grok-build-settings
|
||||
|
||||
POST cli tools › grok build settings
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/cli-tools/grok-build-settings \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### DELETE /api/cli-tools/grok-build-settings
|
||||
|
||||
DELETE cli tools › grok build settings
|
||||
|
||||
```bash
|
||||
curl -X DELETE https://localhost:20128/api/cli-tools/grok-build-settings \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### GET /api/cli-tools/hermes-agent-settings
|
||||
|
||||
GET cli tools › hermes agent settings
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/cli-tools/hermes-agent-settings \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### POST /api/cli-tools/hermes-agent-settings
|
||||
|
||||
POST cli tools › hermes agent settings
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/cli-tools/hermes-agent-settings \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### GET /api/cli-tools/jcode-settings
|
||||
|
||||
GET cli tools › jcode settings
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/cli-tools/jcode-settings \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### POST /api/cli-tools/jcode-settings
|
||||
|
||||
POST cli tools › jcode settings
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/cli-tools/jcode-settings \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### DELETE /api/cli-tools/jcode-settings
|
||||
|
||||
DELETE cli tools › jcode settings
|
||||
|
||||
```bash
|
||||
curl -X DELETE https://localhost:20128/api/cli-tools/jcode-settings \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### GET /api/cli-tools/keys
|
||||
|
||||
GET cli tools › keys
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/cli-tools/keys \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### GET /api/cli-tools/letta-settings
|
||||
|
||||
GET cli tools › letta settings
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/cli-tools/letta-settings \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### POST /api/cli-tools/letta-settings
|
||||
|
||||
POST cli tools › letta settings
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/cli-tools/letta-settings \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### DELETE /api/cli-tools/letta-settings
|
||||
|
||||
DELETE cli tools › letta settings
|
||||
|
||||
```bash
|
||||
curl -X DELETE https://localhost:20128/api/cli-tools/letta-settings \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### GET /api/cli-tools/logs
|
||||
|
||||
GET cli tools › logs
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/cli-tools/logs \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### GET /api/cli-tools/omp-settings
|
||||
|
||||
GET cli tools › omp settings
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/cli-tools/omp-settings \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### POST /api/cli-tools/omp-settings
|
||||
|
||||
POST cli tools › omp settings
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/cli-tools/omp-settings \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### DELETE /api/cli-tools/omp-settings
|
||||
|
||||
DELETE cli tools › omp settings
|
||||
|
||||
```bash
|
||||
curl -X DELETE https://localhost:20128/api/cli-tools/omp-settings \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### GET /api/cli-tools/openclaw/auto-order
|
||||
|
||||
GET cli tools › openclaw › auto order
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/cli-tools/openclaw/auto-order \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### GET /api/cli-tools/pi-settings
|
||||
|
||||
GET cli tools › pi settings
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/cli-tools/pi-settings \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### POST /api/cli-tools/pi-settings
|
||||
|
||||
POST cli tools › pi settings
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/cli-tools/pi-settings \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### DELETE /api/cli-tools/pi-settings
|
||||
|
||||
DELETE cli tools › pi settings
|
||||
|
||||
```bash
|
||||
curl -X DELETE https://localhost:20128/api/cli-tools/pi-settings \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### GET /api/cli-tools/qwen-settings
|
||||
|
||||
GET cli tools › qwen settings
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/cli-tools/qwen-settings \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### POST /api/cli-tools/qwen-settings
|
||||
|
||||
POST cli tools › qwen settings
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/cli-tools/qwen-settings \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### DELETE /api/cli-tools/qwen-settings
|
||||
|
||||
DELETE cli tools › qwen settings
|
||||
|
||||
```bash
|
||||
curl -X DELETE https://localhost:20128/api/cli-tools/qwen-settings \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### GET /api/cli-tools/smelt-settings
|
||||
|
||||
GET cli tools › smelt settings
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/cli-tools/smelt-settings \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### POST /api/cli-tools/smelt-settings
|
||||
|
||||
POST cli tools › smelt settings
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/cli-tools/smelt-settings \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### DELETE /api/cli-tools/smelt-settings
|
||||
|
||||
DELETE cli tools › smelt settings
|
||||
|
||||
```bash
|
||||
curl -X DELETE https://localhost:20128/api/cli-tools/smelt-settings \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### GET /api/cli-tools/status
|
||||
|
||||
GET cli tools › status
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/cli-tools/status \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
## Payloads
|
||||
|
||||
See the full OpenAPI specification at `GET /api/openapi/spec` or `docs/openapi.yaml` for detailed request/response schemas.
|
||||
|
||||
@@ -131,46 +131,6 @@ curl -X DELETE https://localhost:20128/api/fallback/chains \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### GET /api/combos/auto
|
||||
|
||||
GET combos › auto
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/combos/auto \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### GET /api/combos/builder/options
|
||||
|
||||
GET combos › builder › options
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/combos/builder/options \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### POST /api/combos/duplicate
|
||||
|
||||
POST combos › duplicate
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/combos/duplicate \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### POST /api/combos/reorder
|
||||
|
||||
POST combos › reorder
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/combos/reorder \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
## Payloads
|
||||
|
||||
See the full OpenAPI specification at `GET /api/openapi/spec` or `docs/openapi.yaml` for detailed request/response schemas.
|
||||
|
||||
@@ -43,48 +43,6 @@ curl https://localhost:20128/api/compression/rules \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### POST /api/compression/compare
|
||||
|
||||
POST compression › compare
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/compression/compare \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### POST /api/compression/compare/verify
|
||||
|
||||
POST compression › compare › verify
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/compression/compare/verify \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### GET /api/compression/engines
|
||||
|
||||
GET compression › engines
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/compression/engines \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### POST /api/compression/retrieve
|
||||
|
||||
POST compression › retrieve
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/compression/retrieve \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
## Payloads
|
||||
|
||||
See the full OpenAPI specification at `GET /api/openapi/spec` or `docs/openapi.yaml` for detailed request/response schemas.
|
||||
|
||||
@@ -74,24 +74,6 @@ curl https://localhost:20128/api/context/rtk/raw-output/{id} \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### GET /api/context/rtk/discover
|
||||
|
||||
GET context › rtk › discover
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/context/rtk/discover \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### GET /api/context/rtk/learn
|
||||
|
||||
GET context › rtk › learn
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/context/rtk/learn \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
## Payloads
|
||||
|
||||
See the full OpenAPI specification at `GET /api/openapi/spec` or `docs/openapi.yaml` for detailed request/response schemas.
|
||||
|
||||
@@ -14,46 +14,7 @@ All requests require a valid Bearer token or session cookie. Obtain a token via
|
||||
|
||||
## Endpoints
|
||||
|
||||
### GET /api/system/env/repair
|
||||
|
||||
GET system › env › repair
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/system/env/repair \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### POST /api/system/env/repair
|
||||
|
||||
POST system › env › repair
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/system/env/repair \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### GET /api/system/version
|
||||
|
||||
GET system › version
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/system/version \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### POST /api/system/version
|
||||
|
||||
POST system › version
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/system/version \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
_No endpoints mapped for this area yet._
|
||||
## Payloads
|
||||
|
||||
See the full OpenAPI specification at `GET /api/openapi/spec` or `docs/openapi.yaml` for detailed request/response schemas.
|
||||
|
||||
@@ -462,898 +462,6 @@ curl https://localhost:20128/api/v1/provider-plugin-manifest \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### GET /api/v1/{omnirouteCatchAll}
|
||||
|
||||
GET <omnirouteCatchAll>
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/v1/{omnirouteCatchAll} \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### POST /api/v1/{omnirouteCatchAll}
|
||||
|
||||
POST <omnirouteCatchAll>
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/v1/{omnirouteCatchAll} \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### PUT /api/v1/{omnirouteCatchAll}
|
||||
|
||||
PUT <omnirouteCatchAll>
|
||||
|
||||
```bash
|
||||
curl -X PUT https://localhost:20128/api/v1/{omnirouteCatchAll} \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### PATCH /api/v1/{omnirouteCatchAll}
|
||||
|
||||
PATCH <omnirouteCatchAll>
|
||||
|
||||
```bash
|
||||
curl -X PATCH https://localhost:20128/api/v1/{omnirouteCatchAll} \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### DELETE /api/v1/{omnirouteCatchAll}
|
||||
|
||||
DELETE <omnirouteCatchAll>
|
||||
|
||||
```bash
|
||||
curl -X DELETE https://localhost:20128/api/v1/{omnirouteCatchAll} \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### GET /api/v1/accounts/{id}/limits
|
||||
|
||||
GET accounts › <id> › limits
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/v1/accounts/{id}/limits \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### PUT /api/v1/accounts/{id}/limits
|
||||
|
||||
PUT accounts › <id> › limits
|
||||
|
||||
```bash
|
||||
curl -X PUT https://localhost:20128/api/v1/accounts/{id}/limits \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### GET /api/v1/agents/credentials
|
||||
|
||||
GET agents › credentials
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/v1/agents/credentials \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### POST /api/v1/agents/credentials
|
||||
|
||||
POST agents › credentials
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/v1/agents/credentials \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### GET /api/v1/agents/health
|
||||
|
||||
GET agents › health
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/v1/agents/health \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### GET /api/v1/agents/tasks
|
||||
|
||||
GET agents › tasks
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/v1/agents/tasks \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### POST /api/v1/agents/tasks
|
||||
|
||||
POST agents › tasks
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/v1/agents/tasks \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### DELETE /api/v1/agents/tasks
|
||||
|
||||
DELETE agents › tasks
|
||||
|
||||
```bash
|
||||
curl -X DELETE https://localhost:20128/api/v1/agents/tasks \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### GET /api/v1/agents/tasks/{id}
|
||||
|
||||
GET agents › tasks › <id>
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/v1/agents/tasks/{id} \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### POST /api/v1/agents/tasks/{id}
|
||||
|
||||
POST agents › tasks › <id>
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/v1/agents/tasks/{id} \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### DELETE /api/v1/agents/tasks/{id}
|
||||
|
||||
DELETE agents › tasks › <id>
|
||||
|
||||
```bash
|
||||
curl -X DELETE https://localhost:20128/api/v1/agents/tasks/{id} \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### POST /api/v1/antigravity
|
||||
|
||||
POST antigravity
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/v1/antigravity \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### GET /api/v1/auto-combo/{channel}/candidates
|
||||
|
||||
GET auto combo › <channel> › candidates
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/v1/auto-combo/{channel}/candidates \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### GET /api/v1/batches
|
||||
|
||||
GET batches
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/v1/batches \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### POST /api/v1/batches
|
||||
|
||||
POST batches
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/v1/batches \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### GET /api/v1/batches/{id}
|
||||
|
||||
GET batches › <id>
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/v1/batches/{id} \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### DELETE /api/v1/batches/{id}
|
||||
|
||||
DELETE batches › <id>
|
||||
|
||||
```bash
|
||||
curl -X DELETE https://localhost:20128/api/v1/batches/{id} \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### POST /api/v1/batches/{id}/cancel
|
||||
|
||||
POST batches › <id> › cancel
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/v1/batches/{id}/cancel \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### DELETE /api/v1/batches/delete-completed
|
||||
|
||||
DELETE batches › delete completed
|
||||
|
||||
```bash
|
||||
curl -X DELETE https://localhost:20128/api/v1/batches/delete-completed \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### POST /api/v1/classify
|
||||
|
||||
POST classify
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/v1/classify \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### GET /api/v1/combos
|
||||
|
||||
GET combos
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/v1/combos \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### POST /api/v1/completions
|
||||
|
||||
POST completions
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/v1/completions \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### GET /api/v1/files
|
||||
|
||||
GET files
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/v1/files \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### POST /api/v1/files
|
||||
|
||||
POST files
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/v1/files \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### GET /api/v1/files/{id}
|
||||
|
||||
GET files › <id>
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/v1/files/{id} \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### DELETE /api/v1/files/{id}
|
||||
|
||||
DELETE files › <id>
|
||||
|
||||
```bash
|
||||
curl -X DELETE https://localhost:20128/api/v1/files/{id} \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### GET /api/v1/files/{id}/content
|
||||
|
||||
GET files › <id> › content
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/v1/files/{id}/content \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### POST /api/v1/images/edits
|
||||
|
||||
POST images › edits
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/v1/images/edits \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### GET /api/v1/images/upscale
|
||||
|
||||
GET images › upscale
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/v1/images/upscale \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### POST /api/v1/images/upscale
|
||||
|
||||
POST images › upscale
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/v1/images/upscale \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### POST /api/v1/issues/report
|
||||
|
||||
POST issues › report
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/v1/issues/report \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### GET /api/v1/management/proxies
|
||||
|
||||
GET management › proxies
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/v1/management/proxies \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### POST /api/v1/management/proxies
|
||||
|
||||
POST management › proxies
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/v1/management/proxies \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### PATCH /api/v1/management/proxies
|
||||
|
||||
PATCH management › proxies
|
||||
|
||||
```bash
|
||||
curl -X PATCH https://localhost:20128/api/v1/management/proxies \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### DELETE /api/v1/management/proxies
|
||||
|
||||
DELETE management › proxies
|
||||
|
||||
```bash
|
||||
curl -X DELETE https://localhost:20128/api/v1/management/proxies \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### GET /api/v1/management/proxies/assignments
|
||||
|
||||
GET management › proxies › assignments
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/v1/management/proxies/assignments \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### PUT /api/v1/management/proxies/assignments
|
||||
|
||||
PUT management › proxies › assignments
|
||||
|
||||
```bash
|
||||
curl -X PUT https://localhost:20128/api/v1/management/proxies/assignments \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### PUT /api/v1/management/proxies/bulk-assign
|
||||
|
||||
PUT management › proxies › bulk assign
|
||||
|
||||
```bash
|
||||
curl -X PUT https://localhost:20128/api/v1/management/proxies/bulk-assign \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### GET /api/v1/management/proxies/health
|
||||
|
||||
GET management › proxies › health
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/v1/management/proxies/health \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### GET /api/v1/me/status
|
||||
|
||||
GET me › status
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/v1/me/status \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### GET /api/v1/muse-code/models
|
||||
|
||||
GET muse code › models
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/v1/muse-code/models \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### GET /api/v1/music/generations
|
||||
|
||||
GET music › generations
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/v1/music/generations \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### POST /api/v1/music/generations
|
||||
|
||||
POST music › generations
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/v1/music/generations \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### GET /api/v1/providers/{provider}/limits
|
||||
|
||||
GET providers › <provider> › limits
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/v1/providers/{provider}/limits \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### PUT /api/v1/providers/{provider}/limits
|
||||
|
||||
PUT providers › <provider> › limits
|
||||
|
||||
```bash
|
||||
curl -X PUT https://localhost:20128/api/v1/providers/{provider}/limits \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### GET /api/v1/quotas/check
|
||||
|
||||
GET quotas › check
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/v1/quotas/check \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### GET /api/v1/registered-keys
|
||||
|
||||
GET registered keys
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/v1/registered-keys \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### POST /api/v1/registered-keys
|
||||
|
||||
POST registered keys
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/v1/registered-keys \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### GET /api/v1/registered-keys/{id}
|
||||
|
||||
GET registered keys › <id>
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/v1/registered-keys/{id} \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### DELETE /api/v1/registered-keys/{id}
|
||||
|
||||
DELETE registered keys › <id>
|
||||
|
||||
```bash
|
||||
curl -X DELETE https://localhost:20128/api/v1/registered-keys/{id} \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### POST /api/v1/registered-keys/{id}/revoke
|
||||
|
||||
POST registered keys › <id> › revoke
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/v1/registered-keys/{id}/revoke \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### POST /api/v1/relay/chat/completions
|
||||
|
||||
POST relay › chat › completions
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/v1/relay/chat/completions \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### POST /api/v1/relay/chat/completions/bifrost
|
||||
|
||||
POST relay › chat › completions › bifrost
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/v1/relay/chat/completions/bifrost \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### POST /api/v1/responses/{path}
|
||||
|
||||
POST responses › <path>
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/v1/responses/{path} \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### GET /api/v1/search/analytics
|
||||
|
||||
GET search › analytics
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/v1/search/analytics \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### POST /api/v1/segment
|
||||
|
||||
POST segment
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/v1/segment \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### GET /api/v1/video-bridge/drilldown
|
||||
|
||||
GET video bridge › drilldown
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/v1/video-bridge/drilldown \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### DELETE /api/v1/video-bridge/drilldown
|
||||
|
||||
DELETE video bridge › drilldown
|
||||
|
||||
```bash
|
||||
curl -X DELETE https://localhost:20128/api/v1/video-bridge/drilldown \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### GET /api/v1/videos/generations
|
||||
|
||||
GET videos › generations
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/v1/videos/generations \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### POST /api/v1/videos/generations
|
||||
|
||||
POST videos › generations
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/v1/videos/generations \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### GET /api/v1/vscode/{token}
|
||||
|
||||
GET vscode › <token>
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/v1/vscode/{token} \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### POST /api/v1/vscode/{token}/api/chat
|
||||
|
||||
POST vscode › <token> › api › chat
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/v1/vscode/{token}/api/chat \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### POST /api/v1/vscode/{token}/api/show
|
||||
|
||||
POST vscode › <token> › api › show
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/v1/vscode/{token}/api/show \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### GET /api/v1/vscode/{token}/api/tags
|
||||
|
||||
GET vscode › <token> › api › tags
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/v1/vscode/{token}/api/tags \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### GET /api/v1/vscode/{token}/api/version
|
||||
|
||||
GET vscode › <token> › api › version
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/v1/vscode/{token}/api/version \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### POST /api/v1/vscode/{token}/chat/completions
|
||||
|
||||
POST vscode › <token> › chat › completions
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/v1/vscode/{token}/chat/completions \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### GET /api/v1/vscode/{token}/combos
|
||||
|
||||
GET vscode › <token> › combos
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/v1/vscode/{token}/combos \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### GET /api/v1/vscode/{token}/models
|
||||
|
||||
GET vscode › <token> › models
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/v1/vscode/{token}/models \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### POST /api/v1/vscode/{token}/responses
|
||||
|
||||
POST vscode › <token> › responses
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/v1/vscode/{token}/responses \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### POST /api/v1/vscode/{token}/v1/chat/completions
|
||||
|
||||
POST vscode › <token> › v1 › chat › completions
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/v1/vscode/{token}/v1/chat/completions \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### GET /api/v1/vscode/{token}/v1/models
|
||||
|
||||
GET vscode › <token> › v1 › models
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/v1/vscode/{token}/v1/models \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### GET /api/v1/vscode/combos/{token}/{{slug}}
|
||||
|
||||
GET vscode › combos › <token> › <{slug>}
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/v1/vscode/combos/{token}/{{slug}} \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### POST /api/v1/vscode/combos/{token}/{{slug}}
|
||||
|
||||
POST vscode › combos › <token> › <{slug>}
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/v1/vscode/combos/{token}/{{slug}} \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### GET /api/v1/vscode/raw/{token}
|
||||
|
||||
GET vscode › raw › <token>
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/v1/vscode/raw/{token} \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### POST /api/v1/vscode/raw/{token}/api/chat
|
||||
|
||||
POST vscode › raw › <token> › api › chat
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/v1/vscode/raw/{token}/api/chat \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### POST /api/v1/vscode/raw/{token}/api/show
|
||||
|
||||
POST vscode › raw › <token> › api › show
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/v1/vscode/raw/{token}/api/show \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### GET /api/v1/vscode/raw/{token}/api/tags
|
||||
|
||||
GET vscode › raw › <token> › api › tags
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/v1/vscode/raw/{token}/api/tags \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### GET /api/v1/vscode/raw/{token}/api/version
|
||||
|
||||
GET vscode › raw › <token> › api › version
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/v1/vscode/raw/{token}/api/version \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### POST /api/v1/vscode/raw/{token}/chat/completions
|
||||
|
||||
POST vscode › raw › <token> › chat › completions
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/v1/vscode/raw/{token}/chat/completions \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### GET /api/v1/vscode/raw/{token}/combos
|
||||
|
||||
GET vscode › raw › <token> › combos
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/v1/vscode/raw/{token}/combos \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### GET /api/v1/vscode/raw/{token}/models
|
||||
|
||||
GET vscode › raw › <token> › models
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/v1/vscode/raw/{token}/models \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### POST /api/v1/vscode/raw/{token}/responses
|
||||
|
||||
POST vscode › raw › <token> › responses
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/v1/vscode/raw/{token}/responses \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### POST /api/v1/vscode/raw/{token}/v1/chat/completions
|
||||
|
||||
POST vscode › raw › <token> › v1 › chat › completions
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/v1/vscode/raw/{token}/v1/chat/completions \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### GET /api/v1/vscode/raw/{token}/v1/models
|
||||
|
||||
GET vscode › raw › <token> › v1 › models
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/v1/vscode/raw/{token}/v1/models \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### POST /api/v1/web/fetch
|
||||
|
||||
POST web › fetch
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/v1/web/fetch \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
## Payloads
|
||||
|
||||
See the full OpenAPI specification at `GET /api/openapi/spec` or `docs/openapi.yaml` for detailed request/response schemas.
|
||||
|
||||
@@ -14,91 +14,7 @@ All requests require a valid Bearer token or session cookie. Obtain a token via
|
||||
|
||||
## Endpoints
|
||||
|
||||
### GET /api/mcp/audit
|
||||
|
||||
GET mcp › audit
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/mcp/audit \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### GET /api/mcp/audit/stats
|
||||
|
||||
GET mcp › audit › stats
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/mcp/audit/stats \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### GET /api/mcp/sse
|
||||
|
||||
GET mcp › sse
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/mcp/sse \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### POST /api/mcp/sse
|
||||
|
||||
POST mcp › sse
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/mcp/sse \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### GET /api/mcp/status
|
||||
|
||||
GET mcp › status
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/mcp/status \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### GET /api/mcp/stream
|
||||
|
||||
GET mcp › stream
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/mcp/stream \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### POST /api/mcp/stream
|
||||
|
||||
POST mcp › stream
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/mcp/stream \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### DELETE /api/mcp/stream
|
||||
|
||||
DELETE mcp › stream
|
||||
|
||||
```bash
|
||||
curl -X DELETE https://localhost:20128/api/mcp/stream \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### GET /api/mcp/tools
|
||||
|
||||
GET mcp › tools
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/mcp/tools \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
_No endpoints mapped for this area yet._
|
||||
## Payloads
|
||||
|
||||
See the full OpenAPI specification at `GET /api/openapi/spec` or `docs/openapi.yaml` for detailed request/response schemas.
|
||||
|
||||
@@ -54,46 +54,6 @@ curl https://localhost:20128/api/models/catalog \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### GET /api/models/openrouter-catalog
|
||||
|
||||
GET models › openrouter catalog
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/models/openrouter-catalog \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### POST /api/models/test
|
||||
|
||||
POST models › test
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/models/test \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### POST /api/models/test-all
|
||||
|
||||
POST models › test all
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/models/test-all \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### GET /api/v1/models/{model}
|
||||
|
||||
GET models › <model>
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/v1/models/{model} \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
## Payloads
|
||||
|
||||
See the full OpenAPI specification at `GET /api/openapi/spec` or `docs/openapi.yaml` for detailed request/response schemas.
|
||||
|
||||
@@ -229,526 +229,6 @@ curl https://localhost:20128/api/provider-models \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### GET /api/providers/{id}/cc-alias
|
||||
|
||||
GET providers › <id> › cc alias
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/providers/{id}/cc-alias \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### PUT /api/providers/{id}/cc-alias
|
||||
|
||||
PUT providers › <id> › cc alias
|
||||
|
||||
```bash
|
||||
curl -X PUT https://localhost:20128/api/providers/{id}/cc-alias \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### GET /api/providers/{id}/chatgpt-web-codex-doctor
|
||||
|
||||
GET providers › <id> › chatgpt web codex doctor
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/providers/{id}/chatgpt-web-codex-doctor \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### POST /api/providers/{id}/claude-auth/apply-local
|
||||
|
||||
POST providers › <id> › claude auth › apply local
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/providers/{id}/claude-auth/apply-local \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### POST /api/providers/{id}/claude-auth/export
|
||||
|
||||
POST providers › <id> › claude auth › export
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/providers/{id}/claude-auth/export \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### POST /api/providers/{id}/codex-auth/apply-local
|
||||
|
||||
POST providers › <id> › codex auth › apply local
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/providers/{id}/codex-auth/apply-local \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### POST /api/providers/{id}/codex-auth/export
|
||||
|
||||
POST providers › <id> › codex auth › export
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/providers/{id}/codex-auth/export \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### GET /api/providers/{id}/interception-rules
|
||||
|
||||
GET providers › <id> › interception rules
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/providers/{id}/interception-rules \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### PUT /api/providers/{id}/interception-rules
|
||||
|
||||
PUT providers › <id> › interception rules
|
||||
|
||||
```bash
|
||||
curl -X PUT https://localhost:20128/api/providers/{id}/interception-rules \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### DELETE /api/providers/{id}/interception-rules
|
||||
|
||||
DELETE providers › <id> › interception rules
|
||||
|
||||
```bash
|
||||
curl -X DELETE https://localhost:20128/api/providers/{id}/interception-rules \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### POST /api/providers/{id}/login
|
||||
|
||||
POST providers › <id> › login
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/providers/{id}/login \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### GET /api/providers/{id}/param-filters
|
||||
|
||||
GET providers › <id> › param filters
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/providers/{id}/param-filters \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### PUT /api/providers/{id}/param-filters
|
||||
|
||||
PUT providers › <id> › param filters
|
||||
|
||||
```bash
|
||||
curl -X PUT https://localhost:20128/api/providers/{id}/param-filters \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### DELETE /api/providers/{id}/param-filters
|
||||
|
||||
DELETE providers › <id> › param filters
|
||||
|
||||
```bash
|
||||
curl -X DELETE https://localhost:20128/api/providers/{id}/param-filters \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### POST /api/providers/{id}/refresh
|
||||
|
||||
POST providers › <id> › refresh
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/providers/{id}/refresh \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### POST /api/providers/{id}/refresh-cursor
|
||||
|
||||
POST providers › <id> › refresh cursor
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/providers/{id}/refresh-cursor \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### POST /api/providers/{id}/refresh-token
|
||||
|
||||
POST providers › <id> › refresh token
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/providers/{id}/refresh-token \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### POST /api/providers/{id}/sync-models
|
||||
|
||||
POST providers › <id> › sync models
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/providers/{id}/sync-models \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### POST /api/providers/bulk
|
||||
|
||||
POST providers › bulk
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/providers/bulk \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### POST /api/providers/bulk-web-session
|
||||
|
||||
POST providers › bulk web session
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/providers/bulk-web-session \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### POST /api/providers/claude-auth/import
|
||||
|
||||
POST providers › claude auth › import
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/providers/claude-auth/import \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### POST /api/providers/claude-auth/import-bulk
|
||||
|
||||
POST providers › claude auth › import bulk
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/providers/claude-auth/import-bulk \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### POST /api/providers/claude-auth/zip-extract
|
||||
|
||||
POST providers › claude auth › zip extract
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/providers/claude-auth/zip-extract \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### POST /api/providers/codex-auth/import
|
||||
|
||||
POST providers › codex auth › import
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/providers/codex-auth/import \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### POST /api/providers/codex-auth/import-bulk
|
||||
|
||||
POST providers › codex auth › import bulk
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/providers/codex-auth/import-bulk \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### POST /api/providers/codex-auth/zip-extract
|
||||
|
||||
POST providers › codex auth › zip extract
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/providers/codex-auth/zip-extract \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### POST /api/providers/command-code/auth/apply
|
||||
|
||||
POST providers › command code › auth › apply
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/providers/command-code/auth/apply \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### POST /api/providers/command-code/auth/callback
|
||||
|
||||
POST providers › command code › auth › callback
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/providers/command-code/auth/callback \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### POST /api/providers/command-code/auth/start
|
||||
|
||||
POST providers › command code › auth › start
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/providers/command-code/auth/start \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### GET /api/providers/command-code/auth/status
|
||||
|
||||
GET providers › command code › auth › status
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/providers/command-code/auth/status \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### POST /api/providers/command-code/auth/status
|
||||
|
||||
POST providers › command code › auth › status
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/providers/command-code/auth/status \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### GET /api/providers/expiration
|
||||
|
||||
GET providers › expiration
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/providers/expiration \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### GET /api/providers/free-onboarding
|
||||
|
||||
GET providers › free onboarding
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/providers/free-onboarding \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### POST /api/providers/free-onboarding
|
||||
|
||||
POST providers › free onboarding
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/providers/free-onboarding \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### GET /api/providers/health-autopilot
|
||||
|
||||
GET providers › health autopilot
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/providers/health-autopilot \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### POST /api/providers/health-autopilot/actions
|
||||
|
||||
POST providers › health autopilot › actions
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/providers/health-autopilot/actions \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### GET /api/providers/health-matrix
|
||||
|
||||
GET providers › health matrix
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/providers/health-matrix \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### POST /api/providers/import
|
||||
|
||||
POST providers › import
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/providers/import \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### GET /api/providers/openrouter-stats
|
||||
|
||||
GET providers › openrouter stats
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/providers/openrouter-stats \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### GET /api/providers/quota-windows
|
||||
|
||||
GET providers › quota windows
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/providers/quota-windows \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### POST /api/providers/volcengine-plan/connect
|
||||
|
||||
POST providers › volcengine plan › connect
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/providers/volcengine-plan/connect \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### POST /api/providers/volcengine-plan/connect/{sessionId}/cancel
|
||||
|
||||
POST providers › volcengine plan › connect › <sessionId> › cancel
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/providers/volcengine-plan/connect/{sessionId}/cancel \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### POST /api/providers/volcengine-plan/connect/{sessionId}/code
|
||||
|
||||
POST providers › volcengine plan › connect › <sessionId> › code
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/providers/volcengine-plan/connect/{sessionId}/code \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### POST /api/providers/volcengine-plan/connect/{sessionId}/identity
|
||||
|
||||
POST providers › volcengine plan › connect › <sessionId> › identity
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/providers/volcengine-plan/connect/{sessionId}/identity \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### POST /api/providers/volcengine-plan/connect/{sessionId}/resend
|
||||
|
||||
POST providers › volcengine plan › connect › <sessionId> › resend
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/providers/volcengine-plan/connect/{sessionId}/resend \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### GET /api/providers/volcengine-plan/connect/{sessionId}/status
|
||||
|
||||
GET providers › volcengine plan › connect › <sessionId> › status
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/providers/volcengine-plan/connect/{sessionId}/status \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### GET /api/providers/web-session-contract
|
||||
|
||||
GET providers › web session contract
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/providers/web-session-contract \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### POST /api/providers/zed/discover
|
||||
|
||||
POST providers › zed › discover
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/providers/zed/discover \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### POST /api/providers/zed/import
|
||||
|
||||
POST providers › zed › import
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/providers/zed/import \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### POST /api/providers/zed/manual-import
|
||||
|
||||
POST providers › zed › manual import
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/providers/zed/manual-import \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
## Payloads
|
||||
|
||||
See the full OpenAPI specification at `GET /api/openapi/spec` or `docs/openapi.yaml` for detailed request/response schemas.
|
||||
|
||||
@@ -25,15 +25,6 @@ curl https://localhost:20128/api/monitoring/health \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### GET /api/provider-metrics
|
||||
|
||||
GET provider metrics
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/provider-metrics \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
## Payloads
|
||||
|
||||
See the full OpenAPI specification at `GET /api/openapi/spec` or `docs/openapi.yaml` for detailed request/response schemas.
|
||||
|
||||
@@ -383,981 +383,6 @@ curl -X POST https://localhost:20128/api/settings/purge-usage-history \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### GET /api/settings/authz-inventory
|
||||
|
||||
GET settings › authz inventory
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/settings/authz-inventory \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### GET /api/settings/auto-disable-accounts
|
||||
|
||||
GET settings › auto disable accounts
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/settings/auto-disable-accounts \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### PUT /api/settings/auto-disable-accounts
|
||||
|
||||
PUT settings › auto disable accounts
|
||||
|
||||
```bash
|
||||
curl -X PUT https://localhost:20128/api/settings/auto-disable-accounts \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### GET /api/settings/background-degradation
|
||||
|
||||
GET settings › background degradation
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/settings/background-degradation \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### POST /api/settings/background-degradation
|
||||
|
||||
POST settings › background degradation
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/settings/background-degradation \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### PUT /api/settings/background-degradation
|
||||
|
||||
PUT settings › background degradation
|
||||
|
||||
```bash
|
||||
curl -X PUT https://localhost:20128/api/settings/background-degradation \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### GET /api/settings/cache-config
|
||||
|
||||
GET settings › cache config
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/settings/cache-config \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### PUT /api/settings/cache-config
|
||||
|
||||
PUT settings › cache config
|
||||
|
||||
```bash
|
||||
curl -X PUT https://localhost:20128/api/settings/cache-config \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### GET /api/settings/cache-metrics
|
||||
|
||||
GET settings › cache metrics
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/settings/cache-metrics \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### DELETE /api/settings/cache-metrics
|
||||
|
||||
DELETE settings › cache metrics
|
||||
|
||||
```bash
|
||||
curl -X DELETE https://localhost:20128/api/settings/cache-metrics \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### GET /api/settings/cc-discovery-metrics
|
||||
|
||||
GET settings › cc discovery metrics
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/settings/cc-discovery-metrics \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### GET /api/settings/compression/rules
|
||||
|
||||
GET settings › compression › rules
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/settings/compression/rules \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### GET /api/settings/compression/run-telemetry
|
||||
|
||||
GET settings › compression › run telemetry
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/settings/compression/run-telemetry \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### GET /api/settings/database
|
||||
|
||||
GET settings › database
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/settings/database \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### PUT /api/settings/database
|
||||
|
||||
PUT settings › database
|
||||
|
||||
```bash
|
||||
curl -X PUT https://localhost:20128/api/settings/database \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### PATCH /api/settings/database
|
||||
|
||||
PATCH settings › database
|
||||
|
||||
```bash
|
||||
curl -X PATCH https://localhost:20128/api/settings/database \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### POST /api/settings/database/refresh-stats
|
||||
|
||||
POST settings › database › refresh stats
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/settings/database/refresh-stats \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### GET /api/settings/database/vacuum
|
||||
|
||||
GET settings › database › vacuum
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/settings/database/vacuum \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### POST /api/settings/database/vacuum
|
||||
|
||||
POST settings › database › vacuum
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/settings/database/vacuum \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### GET /api/settings/export-json
|
||||
|
||||
GET settings › export json
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/settings/export-json \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### GET /api/settings/favicon
|
||||
|
||||
GET settings › favicon
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/settings/favicon \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### GET /api/settings/feature-flags
|
||||
|
||||
GET settings › feature flags
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/settings/feature-flags \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### PUT /api/settings/feature-flags
|
||||
|
||||
PUT settings › feature flags
|
||||
|
||||
```bash
|
||||
curl -X PUT https://localhost:20128/api/settings/feature-flags \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### DELETE /api/settings/feature-flags
|
||||
|
||||
DELETE settings › feature flags
|
||||
|
||||
```bash
|
||||
curl -X DELETE https://localhost:20128/api/settings/feature-flags \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### GET /api/settings/free-proxies
|
||||
|
||||
GET settings › free proxies
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/settings/free-proxies \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### DELETE /api/settings/free-proxies
|
||||
|
||||
DELETE settings › free proxies
|
||||
|
||||
```bash
|
||||
curl -X DELETE https://localhost:20128/api/settings/free-proxies \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### POST /api/settings/free-proxies/{id}/add-to-pool
|
||||
|
||||
POST settings › free proxies › <id> › add to pool
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/settings/free-proxies/{id}/add-to-pool \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### POST /api/settings/free-proxies/bulk-add-to-pool
|
||||
|
||||
POST settings › free proxies › bulk add to pool
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/settings/free-proxies/bulk-add-to-pool \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### GET /api/settings/free-proxies/stats
|
||||
|
||||
GET settings › free proxies › stats
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/settings/free-proxies/stats \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### POST /api/settings/free-proxies/sync
|
||||
|
||||
POST settings › free proxies › sync
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/settings/free-proxies/sync \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### POST /api/settings/import-json
|
||||
|
||||
POST settings › import json
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/settings/import-json \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### DELETE /api/settings/lkgp-cache
|
||||
|
||||
DELETE settings › lkgp cache
|
||||
|
||||
```bash
|
||||
curl -X DELETE https://localhost:20128/api/settings/lkgp-cache \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### GET /api/settings/local-corpus
|
||||
|
||||
GET settings › local corpus
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/settings/local-corpus \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### POST /api/settings/local-corpus
|
||||
|
||||
POST settings › local corpus
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/settings/local-corpus \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### DELETE /api/settings/local-corpus
|
||||
|
||||
DELETE settings › local corpus
|
||||
|
||||
```bash
|
||||
curl -X DELETE https://localhost:20128/api/settings/local-corpus \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### GET /api/settings/mitm
|
||||
|
||||
GET settings › mitm
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/settings/mitm \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### POST /api/settings/mitm
|
||||
|
||||
POST settings › mitm
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/settings/mitm \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### PUT /api/settings/mitm
|
||||
|
||||
PUT settings › mitm
|
||||
|
||||
```bash
|
||||
curl -X PUT https://localhost:20128/api/settings/mitm \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### GET /api/settings/model-aliases
|
||||
|
||||
GET settings › model aliases
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/settings/model-aliases \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### POST /api/settings/model-aliases
|
||||
|
||||
POST settings › model aliases
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/settings/model-aliases \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### PUT /api/settings/model-aliases
|
||||
|
||||
PUT settings › model aliases
|
||||
|
||||
```bash
|
||||
curl -X PUT https://localhost:20128/api/settings/model-aliases \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### DELETE /api/settings/model-aliases
|
||||
|
||||
DELETE settings › model aliases
|
||||
|
||||
```bash
|
||||
curl -X DELETE https://localhost:20128/api/settings/model-aliases \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### GET /api/settings/models-dev
|
||||
|
||||
GET settings › models dev
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/settings/models-dev \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### POST /api/settings/models-dev
|
||||
|
||||
POST settings › models dev
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/settings/models-dev \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### GET /api/settings/notion
|
||||
|
||||
GET settings › notion
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/settings/notion \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### POST /api/settings/notion
|
||||
|
||||
POST settings › notion
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/settings/notion \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### DELETE /api/settings/notion
|
||||
|
||||
DELETE settings › notion
|
||||
|
||||
```bash
|
||||
curl -X DELETE https://localhost:20128/api/settings/notion \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### GET /api/settings/obsidian
|
||||
|
||||
GET settings › obsidian
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/settings/obsidian \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### POST /api/settings/obsidian
|
||||
|
||||
POST settings › obsidian
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/settings/obsidian \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### DELETE /api/settings/obsidian
|
||||
|
||||
DELETE settings › obsidian
|
||||
|
||||
```bash
|
||||
curl -X DELETE https://localhost:20128/api/settings/obsidian \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### GET /api/settings/obsidian/webdav
|
||||
|
||||
GET settings › obsidian › webdav
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/settings/obsidian/webdav \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### POST /api/settings/obsidian/webdav
|
||||
|
||||
POST settings › obsidian › webdav
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/settings/obsidian/webdav \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### DELETE /api/settings/obsidian/webdav
|
||||
|
||||
DELETE settings › obsidian › webdav
|
||||
|
||||
```bash
|
||||
curl -X DELETE https://localhost:20128/api/settings/obsidian/webdav \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### GET /api/settings/oneproxy
|
||||
|
||||
GET settings › oneproxy
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/settings/oneproxy \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### POST /api/settings/oneproxy
|
||||
|
||||
POST settings › oneproxy
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/settings/oneproxy \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### DELETE /api/settings/oneproxy
|
||||
|
||||
DELETE settings › oneproxy
|
||||
|
||||
```bash
|
||||
curl -X DELETE https://localhost:20128/api/settings/oneproxy \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### POST /api/settings/oneproxy/rotate
|
||||
|
||||
POST settings › oneproxy › rotate
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/settings/oneproxy/rotate \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### GET /api/settings/proxies
|
||||
|
||||
GET settings › proxies
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/settings/proxies \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### POST /api/settings/proxies
|
||||
|
||||
POST settings › proxies
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/settings/proxies \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### PATCH /api/settings/proxies
|
||||
|
||||
PATCH settings › proxies
|
||||
|
||||
```bash
|
||||
curl -X PATCH https://localhost:20128/api/settings/proxies \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### DELETE /api/settings/proxies
|
||||
|
||||
DELETE settings › proxies
|
||||
|
||||
```bash
|
||||
curl -X DELETE https://localhost:20128/api/settings/proxies \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### POST /api/settings/proxies/{id}/repair-relay
|
||||
|
||||
POST settings › proxies › <id> › repair relay
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/settings/proxies/{id}/repair-relay \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### GET /api/settings/proxies/assignments
|
||||
|
||||
GET settings › proxies › assignments
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/settings/proxies/assignments \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### PUT /api/settings/proxies/assignments
|
||||
|
||||
PUT settings › proxies › assignments
|
||||
|
||||
```bash
|
||||
curl -X PUT https://localhost:20128/api/settings/proxies/assignments \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### POST /api/settings/proxies/auto-test
|
||||
|
||||
POST settings › proxies › auto test
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/settings/proxies/auto-test \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### POST /api/settings/proxies/batch-activate
|
||||
|
||||
POST settings › proxies › batch activate
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/settings/proxies/batch-activate \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### POST /api/settings/proxies/batch-delete
|
||||
|
||||
POST settings › proxies › batch delete
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/settings/proxies/batch-delete \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### PUT /api/settings/proxies/bulk-assign
|
||||
|
||||
PUT settings › proxies › bulk assign
|
||||
|
||||
```bash
|
||||
curl -X PUT https://localhost:20128/api/settings/proxies/bulk-assign \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### POST /api/settings/proxies/bulk-import
|
||||
|
||||
POST settings › proxies › bulk import
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/settings/proxies/bulk-import \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### GET /api/settings/proxies/egress
|
||||
|
||||
GET settings › proxies › egress
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/settings/proxies/egress \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### POST /api/settings/proxies/egress
|
||||
|
||||
POST settings › proxies › egress
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/settings/proxies/egress \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### GET /api/settings/proxies/health
|
||||
|
||||
GET settings › proxies › health
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/settings/proxies/health \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### POST /api/settings/proxies/migrate
|
||||
|
||||
POST settings › proxies › migrate
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/settings/proxies/migrate \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### GET /api/settings/proxies/pool
|
||||
|
||||
GET settings › proxies › pool
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/settings/proxies/pool \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### PUT /api/settings/proxies/pool
|
||||
|
||||
PUT settings › proxies › pool
|
||||
|
||||
```bash
|
||||
curl -X PUT https://localhost:20128/api/settings/proxies/pool \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### PATCH /api/settings/proxies/pool
|
||||
|
||||
PATCH settings › proxies › pool
|
||||
|
||||
```bash
|
||||
curl -X PATCH https://localhost:20128/api/settings/proxies/pool \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### DELETE /api/settings/proxies/pool
|
||||
|
||||
DELETE settings › proxies › pool
|
||||
|
||||
```bash
|
||||
curl -X DELETE https://localhost:20128/api/settings/proxies/pool \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### POST /api/settings/proxy/cloudflare-deploy
|
||||
|
||||
POST settings › proxy › cloudflare deploy
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/settings/proxy/cloudflare-deploy \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### POST /api/settings/proxy/deno-deploy
|
||||
|
||||
POST settings › proxy › deno deploy
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/settings/proxy/deno-deploy \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### POST /api/settings/proxy/vercel-deploy
|
||||
|
||||
POST settings › proxy › vercel deploy
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/settings/proxy/vercel-deploy \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### POST /api/settings/purge-call-logs
|
||||
|
||||
POST settings › purge call logs
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/settings/purge-call-logs \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### POST /api/settings/purge-detailed-logs
|
||||
|
||||
POST settings › purge detailed logs
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/settings/purge-detailed-logs \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### POST /api/settings/purge-logs
|
||||
|
||||
POST settings › purge logs
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/settings/purge-logs \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### POST /api/settings/purge-quota-snapshots
|
||||
|
||||
POST settings › purge quota snapshots
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/settings/purge-quota-snapshots \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### GET /api/settings/quota/state
|
||||
|
||||
GET settings › quota › state
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/settings/quota/state \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### POST /api/settings/quota/state
|
||||
|
||||
POST settings › quota › state
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/settings/quota/state \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### GET /api/settings/reasoning-routing-rules
|
||||
|
||||
GET settings › reasoning routing rules
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/settings/reasoning-routing-rules \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### POST /api/settings/reasoning-routing-rules
|
||||
|
||||
POST settings › reasoning routing rules
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/settings/reasoning-routing-rules \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### GET /api/settings/reasoning-routing-rules/{id}
|
||||
|
||||
GET settings › reasoning routing rules › <id>
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/settings/reasoning-routing-rules/{id} \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### PATCH /api/settings/reasoning-routing-rules/{id}
|
||||
|
||||
PATCH settings › reasoning routing rules › <id>
|
||||
|
||||
```bash
|
||||
curl -X PATCH https://localhost:20128/api/settings/reasoning-routing-rules/{id} \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### DELETE /api/settings/reasoning-routing-rules/{id}
|
||||
|
||||
DELETE settings › reasoning routing rules › <id>
|
||||
|
||||
```bash
|
||||
curl -X DELETE https://localhost:20128/api/settings/reasoning-routing-rules/{id} \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### POST /api/settings/reasoning-routing-rules/simulate
|
||||
|
||||
POST settings › reasoning routing rules › simulate
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/settings/reasoning-routing-rules/simulate \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### GET /api/settings/task-routing
|
||||
|
||||
GET settings › task routing
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/settings/task-routing \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### POST /api/settings/task-routing
|
||||
|
||||
POST settings › task routing
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/settings/task-routing \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### PUT /api/settings/task-routing
|
||||
|
||||
PUT settings › task routing
|
||||
|
||||
```bash
|
||||
curl -X PUT https://localhost:20128/api/settings/task-routing \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### GET /api/settings/tier-config
|
||||
|
||||
GET settings › tier config
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/settings/tier-config \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### PUT /api/settings/tier-config
|
||||
|
||||
PUT settings › tier config
|
||||
|
||||
```bash
|
||||
curl -X PUT https://localhost:20128/api/settings/tier-config \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
## Payloads
|
||||
|
||||
See the full OpenAPI specification at `GET /api/openapi/spec` or `docs/openapi.yaml` for detailed request/response schemas.
|
||||
|
||||
@@ -93,44 +93,6 @@ curl -X POST https://localhost:20128/api/sync/initialize \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### GET /api/sync/bundle
|
||||
|
||||
GET sync › bundle
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/sync/bundle \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### GET /api/sync/tokens
|
||||
|
||||
GET sync › tokens
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/sync/tokens \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### POST /api/sync/tokens
|
||||
|
||||
POST sync › tokens
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/sync/tokens \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### DELETE /api/sync/tokens/{id}
|
||||
|
||||
DELETE sync › tokens › <id>
|
||||
|
||||
```bash
|
||||
curl -X DELETE https://localhost:20128/api/sync/tokens/{id} \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
## Payloads
|
||||
|
||||
See the full OpenAPI specification at `GET /api/openapi/spec` or `docs/openapi.yaml` for detailed request/response schemas.
|
||||
|
||||
@@ -137,192 +137,6 @@ curl https://localhost:20128/api/usage/model-latency-stats \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### GET /api/usage/budget/bulk
|
||||
|
||||
GET usage › budget › bulk
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/usage/budget/bulk \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### GET /api/usage/codex-reset-credit
|
||||
|
||||
GET usage › codex reset credit
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/usage/codex-reset-credit \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### POST /api/usage/codex-reset-credit
|
||||
|
||||
POST usage › codex reset credit
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/usage/codex-reset-credit \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### GET /api/usage/combo-forecast
|
||||
|
||||
GET usage › combo forecast
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/usage/combo-forecast \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### GET /api/usage/combo-health
|
||||
|
||||
GET usage › combo health
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/usage/combo-health \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### GET /api/usage/combo-health-autopilot
|
||||
|
||||
GET usage › combo health autopilot
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/usage/combo-health-autopilot \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### GET /api/usage/combo-health-dashboard
|
||||
|
||||
GET usage › combo health dashboard
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/usage/combo-health-dashboard \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### GET /api/usage/combo-scoring-inspector
|
||||
|
||||
GET usage › combo scoring inspector
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/usage/combo-scoring-inspector \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### GET /api/usage/combo-trace/{id}
|
||||
|
||||
GET usage › combo trace › <id>
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/usage/combo-trace/{id} \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### GET /api/usage/om-usage
|
||||
|
||||
GET usage › om usage
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/usage/om-usage \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### GET /api/usage/provider-limits
|
||||
|
||||
GET usage › provider limits
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/usage/provider-limits \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### POST /api/usage/provider-limits
|
||||
|
||||
POST usage › provider limits
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/usage/provider-limits \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### GET /api/usage/provider-window-costs
|
||||
|
||||
GET usage › provider window costs
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/usage/provider-window-costs \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### GET /api/usage/quota
|
||||
|
||||
GET usage › quota
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/usage/quota \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### GET /api/usage/requests-by-provider-date
|
||||
|
||||
GET usage › requests by provider date
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/usage/requests-by-provider-date \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### GET /api/usage/route-explain/{id}
|
||||
|
||||
GET usage › route explain › <id>
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/usage/route-explain/{id} \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### GET /api/usage/token-limits
|
||||
|
||||
GET usage › token limits
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/usage/token-limits \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### POST /api/usage/token-limits
|
||||
|
||||
POST usage › token limits
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/usage/token-limits \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### DELETE /api/usage/token-limits
|
||||
|
||||
DELETE usage › token limits
|
||||
|
||||
```bash
|
||||
curl -X DELETE https://localhost:20128/api/usage/token-limits \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### GET /api/usage/utilization
|
||||
|
||||
GET usage › utilization
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/usage/utilization \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
## Payloads
|
||||
|
||||
See the full OpenAPI specification at `GET /api/openapi/spec` or `docs/openapi.yaml` for detailed request/response schemas.
|
||||
|
||||
@@ -620,46 +620,6 @@ curl https://localhost:20128/api/services/{name}/logs \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### GET /api/services/9router/models
|
||||
|
||||
GET services › 9router › models
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/services/9router/models \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### POST /api/services/9router/provider-expose
|
||||
|
||||
POST services › 9router › provider expose
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/services/9router/provider-expose \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### GET /api/services/cliproxy/accounts
|
||||
|
||||
GET services › cliproxy › accounts
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/services/cliproxy/accounts \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### POST /api/services/cliproxy/provider-expose
|
||||
|
||||
POST services › cliproxy › provider expose
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/services/cliproxy/provider-expose \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
## Payloads
|
||||
|
||||
See the full OpenAPI specification at `GET /api/openapi/spec` or `docs/openapi.yaml` for detailed request/response schemas.
|
||||
|
||||
@@ -14,86 +14,7 @@ All requests require a valid Bearer token or session cookie. Obtain a token via
|
||||
|
||||
## Endpoints
|
||||
|
||||
### GET /api/webhooks
|
||||
|
||||
GET webhooks
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/webhooks \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### POST /api/webhooks
|
||||
|
||||
POST webhooks
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/webhooks \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### GET /api/webhooks/{id}
|
||||
|
||||
GET webhooks › <id>
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/webhooks/{id} \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### PUT /api/webhooks/{id}
|
||||
|
||||
PUT webhooks › <id>
|
||||
|
||||
```bash
|
||||
curl -X PUT https://localhost:20128/api/webhooks/{id} \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### DELETE /api/webhooks/{id}
|
||||
|
||||
DELETE webhooks › <id>
|
||||
|
||||
```bash
|
||||
curl -X DELETE https://localhost:20128/api/webhooks/{id} \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### GET /api/webhooks/{id}/deliveries
|
||||
|
||||
GET webhooks › <id> › deliveries
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/webhooks/{id}/deliveries \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### POST /api/webhooks/{id}/test
|
||||
|
||||
POST webhooks › <id> › test
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/webhooks/{id}/test \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### POST /api/webhooks/validate-url
|
||||
|
||||
POST webhooks › validate url
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/webhooks/validate-url \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
_No endpoints mapped for this area yet._
|
||||
## Payloads
|
||||
|
||||
See the full OpenAPI specification at `GET /api/openapi/spec` or `docs/openapi.yaml` for detailed request/response schemas.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
import { useState, useEffect, useMemo, useCallback, useRef } from "react";
|
||||
import { useState, useEffect, useMemo, useCallback, useRef, useSyncExternalStore } from "react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Card, CardSkeleton, Button, Modal } from "@/shared/components";
|
||||
@@ -106,6 +106,12 @@ const INLINE_LINK = "text-primary hover:underline";
|
||||
const DOCS_LINK =
|
||||
"hidden sm:inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium border border-border text-text-muted hover:text-text-main hover:bg-bg-subtle transition-colors";
|
||||
|
||||
// Stable no-op subscription for useSyncExternalStore reads of never-changing
|
||||
// browser globals (location.origin does not change without a full navigation).
|
||||
function emptySubscribe() {
|
||||
return () => {};
|
||||
}
|
||||
|
||||
export default function HomePageClient({ machineId }: HomePageClientProps) {
|
||||
const router = useRouter();
|
||||
const isElectron = useIsElectron();
|
||||
@@ -115,7 +121,13 @@ export default function HomePageClient({ machineId }: HomePageClientProps) {
|
||||
const [providerConnections, setProviderConnections] = useState([]);
|
||||
const [models, setModels] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [baseUrl, setBaseUrl] = useState("/v1");
|
||||
// useSyncExternalStore keeps SSR/hydration consistent ("/v1" on the server,
|
||||
// the real origin after hydration) without a setState-in-effect round-trip.
|
||||
const baseUrl = useSyncExternalStore(
|
||||
emptySubscribe,
|
||||
() => `${globalThis.location.origin}/v1`,
|
||||
() => "/v1"
|
||||
);
|
||||
const [selectedProvider, setSelectedProvider] = useState(null);
|
||||
const [providerMetrics, setProviderMetrics] = useState<Record<string, ProviderMetricSummary>>({});
|
||||
const [providerTopology, setProviderTopology] = useState({ lastProvider: "", errorProvider: "" });
|
||||
@@ -135,36 +147,39 @@ export default function HomePageClient({ machineId }: HomePageClientProps) {
|
||||
// Platform detection and download links for Electron
|
||||
const platform =
|
||||
typeof globalThis.window === "undefined" ? undefined : globalThis.window.electronAPI?.platform;
|
||||
// Destructured to locals: `versionInfo?.current` in a dependency array trips
|
||||
// the lint heuristic that treats any `.current` access as a mutable ref read.
|
||||
const installedVersion = versionInfo?.current || "";
|
||||
const latestVersion = versionInfo?.latest || "";
|
||||
const electronDownload = useMemo(() => {
|
||||
const latest = versionInfo?.latest || "";
|
||||
const cleanLatest = latest.replace(/^v/, "");
|
||||
const cleanLatest = latestVersion.replace(/^v/, "");
|
||||
if (platform === "darwin") {
|
||||
return {
|
||||
label: t("downloadDmg"),
|
||||
url: `https://github.com/diegosouzapw/OmniRoute/releases/download/v${cleanLatest}/OmniRoute-${cleanLatest}.dmg`,
|
||||
desc: t("downloadDmgDescription", { version: versionInfo?.current || "" }),
|
||||
desc: t("downloadDmgDescription", { version: installedVersion }),
|
||||
};
|
||||
}
|
||||
if (platform === "win32") {
|
||||
return {
|
||||
label: t("downloadExe"),
|
||||
url: `https://github.com/diegosouzapw/OmniRoute/releases/download/v${cleanLatest}/OmniRoute.Setup.${cleanLatest}.exe`,
|
||||
desc: t("downloadExeDescription", { version: versionInfo?.current || "" }),
|
||||
desc: t("downloadExeDescription", { version: installedVersion }),
|
||||
};
|
||||
}
|
||||
if (platform === "linux") {
|
||||
return {
|
||||
label: t("downloadAppImage"),
|
||||
url: `https://github.com/diegosouzapw/OmniRoute/releases/download/v${cleanLatest}/OmniRoute-${cleanLatest}.AppImage`,
|
||||
desc: t("downloadAppImageDescription", { version: versionInfo?.current || "" }),
|
||||
desc: t("downloadAppImageDescription", { version: installedVersion }),
|
||||
};
|
||||
}
|
||||
return {
|
||||
label: t("downloadUpdate"),
|
||||
url: `https://github.com/diegosouzapw/OmniRoute/releases/tag/v${cleanLatest}`,
|
||||
desc: t("downloadUpdateDescription", { version: versionInfo?.current || "" }),
|
||||
desc: t("downloadUpdateDescription", { version: installedVersion }),
|
||||
};
|
||||
}, [platform, t, versionInfo?.latest, versionInfo?.current]);
|
||||
}, [platform, t, latestVersion, installedVersion]);
|
||||
|
||||
// Electron internal auto-updater state and listeners
|
||||
const [electronUpdateStatus, setElectronUpdateStatus] = useState<{
|
||||
@@ -234,12 +249,6 @@ export default function HomePageClient({ machineId }: HomePageClientProps) {
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof globalThis.window !== "undefined") {
|
||||
setBaseUrl(`${globalThis.location.origin}/v1`);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
try {
|
||||
const [provRes, modelsRes, versionRes] = await Promise.all([
|
||||
@@ -267,7 +276,9 @@ export default function HomePageClient({ machineId }: HomePageClientProps) {
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
void (async () => {
|
||||
await fetchData();
|
||||
})();
|
||||
}, [fetchData]);
|
||||
|
||||
// Fetch provider nodes for display labels (compat providers)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useCallback, useRef } from "react";
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import type { AuditLogEntry } from "@/lib/compliance/index";
|
||||
import ActivityFeed from "./components/ActivityFeed";
|
||||
@@ -14,7 +14,9 @@ export default function ActivityFeedClient() {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [category, setCategory] = useState<EventCategory>("all");
|
||||
const referenceNowMs = useRef<number>(Date.now());
|
||||
// State (not a ref) because it is rendered: refs cannot be read during
|
||||
// render, and Date.now() cannot run there either — the fetch settles it.
|
||||
const [referenceNowMs, setReferenceNowMs] = useState<number>(0);
|
||||
|
||||
const fetchEntries = useCallback(async () => {
|
||||
setLoading(true);
|
||||
@@ -30,7 +32,7 @@ export default function ActivityFeedClient() {
|
||||
}
|
||||
const data = (await res.json()) as AuditLogEntry[];
|
||||
// Reset reference time on fresh load so relative timestamps are stable
|
||||
referenceNowMs.current = Date.now();
|
||||
setReferenceNowMs(Date.now());
|
||||
setAllEntries(Array.isArray(data) ? data : []);
|
||||
} catch (err: unknown) {
|
||||
const msg = err instanceof Error ? err.message : t("fetchFailed");
|
||||
@@ -41,7 +43,9 @@ export default function ActivityFeedClient() {
|
||||
}, [t]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchEntries();
|
||||
void (async () => {
|
||||
await fetchEntries();
|
||||
})();
|
||||
}, [fetchEntries]);
|
||||
|
||||
const filtered =
|
||||
@@ -114,7 +118,7 @@ export default function ActivityFeedClient() {
|
||||
<span className="text-sm">{t("loadingActivity")}</span>
|
||||
</div>
|
||||
) : (
|
||||
<ActivityFeed entries={filtered} referenceNowMs={referenceNowMs.current} />
|
||||
<ActivityFeed entries={filtered} referenceNowMs={referenceNowMs} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -95,7 +95,9 @@ export default function CacheHealthTab() {
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
void load(range);
|
||||
void (async () => {
|
||||
await load(range);
|
||||
})();
|
||||
}, [load, range]);
|
||||
|
||||
if (loading) return <Skeleton className="h-64 w-full" />;
|
||||
@@ -179,8 +181,8 @@ export default function CacheHealthTab() {
|
||||
{text(t, "cacheHealthConcentration", "Where the writes are concentrated")}
|
||||
</h3>
|
||||
<span className="text-xs text-text-muted">
|
||||
{text(t, "cacheHealthThreshold", "outlier above")} {compact(data.heavyWriteThreshold)}{" "}
|
||||
{text(t, "cacheHealthTokens", "tokens")}
|
||||
{text(t, "cacheHealthThreshold", "outlier above")}{" "}
|
||||
{compact(data.heavyWriteThreshold)} {text(t, "cacheHealthTokens", "tokens")}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm text-text-main">
|
||||
@@ -216,7 +218,9 @@ export default function CacheHealthTab() {
|
||||
<table className="w-full min-w-[560px] text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-border text-left text-xs uppercase text-text-muted">
|
||||
<th className="pb-2 pr-4 font-medium">{text(t, "cacheHealthModel", "Model")}</th>
|
||||
<th className="pb-2 pr-4 font-medium">
|
||||
{text(t, "cacheHealthModel", "Model")}
|
||||
</th>
|
||||
<th className="pb-2 pr-4 text-right font-medium">
|
||||
{text(t, "cacheHealthCalls", "Calls")}
|
||||
</th>
|
||||
|
||||
@@ -852,7 +852,9 @@ export default function ComboHealthTab() {
|
||||
|
||||
useEffect(() => {
|
||||
const controller = new AbortController();
|
||||
fetchData(controller, false);
|
||||
void (async () => {
|
||||
await fetchData(controller, false);
|
||||
})();
|
||||
return () => controller.abort();
|
||||
}, [fetchData]);
|
||||
|
||||
|
||||
@@ -131,7 +131,9 @@ export default function ProviderUtilizationTab() {
|
||||
useEffect(() => {
|
||||
const controller = new AbortController();
|
||||
|
||||
fetchUtilization(range, aggregateBy, controller.signal);
|
||||
void (async () => {
|
||||
await fetchUtilization(range, aggregateBy, controller.signal);
|
||||
})();
|
||||
|
||||
return () => controller.abort();
|
||||
}, [fetchUtilization, range, aggregateBy]);
|
||||
@@ -340,12 +342,8 @@ export default function ProviderUtilizationTab() {
|
||||
<ProviderIcon providerId={providerPart} size={22} />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-text-main">
|
||||
{cardTitle}
|
||||
</p>
|
||||
<p className="text-xs text-text-muted">
|
||||
{cardSubtitle}
|
||||
</p>
|
||||
<p className="text-sm font-semibold text-text-main">{cardTitle}</p>
|
||||
<p className="text-xs text-text-muted">{cardSubtitle}</p>
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
|
||||
@@ -522,14 +522,18 @@ export default function RouteExplainabilityTab({
|
||||
|
||||
useEffect(() => {
|
||||
const controller = new AbortController();
|
||||
fetchLogs(controller.signal);
|
||||
void (async () => {
|
||||
await fetchLogs(controller.signal);
|
||||
})();
|
||||
return () => controller.abort();
|
||||
}, [fetchLogs]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedId) return;
|
||||
const controller = new AbortController();
|
||||
fetchExplanation(selectedId, controller.signal);
|
||||
void (async () => {
|
||||
await fetchExplanation(selectedId, controller.signal);
|
||||
})();
|
||||
return () => controller.abort();
|
||||
}, [fetchExplanation, selectedId]);
|
||||
|
||||
|
||||
@@ -64,7 +64,9 @@ export default function A2aAuditTab() {
|
||||
}, [offset, skillFilter, stateFilter]);
|
||||
|
||||
useEffect(() => {
|
||||
void fetchTasks();
|
||||
void (async () => {
|
||||
await fetchTasks();
|
||||
})();
|
||||
}, [fetchTasks]);
|
||||
|
||||
return (
|
||||
|
||||
@@ -110,7 +110,9 @@ export default function ComplianceTab() {
|
||||
}, [actor, eventType, from, offset, t, to]);
|
||||
|
||||
useEffect(() => {
|
||||
void fetchEntries();
|
||||
void (async () => {
|
||||
await fetchEntries();
|
||||
})();
|
||||
}, [fetchEntries]);
|
||||
|
||||
const visibleEntries = useMemo(() => {
|
||||
@@ -330,7 +332,9 @@ export default function ComplianceTab() {
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<span className="rounded-md border border-border bg-surface px-2 py-1 font-mono text-xs text-text-main">
|
||||
{t.has(`eventTypes.${entry.action}`) ? t(`eventTypes.${entry.action}`) : entry.action}
|
||||
{t.has(`eventTypes.${entry.action}`)
|
||||
? t(`eventTypes.${entry.action}`)
|
||||
: entry.action}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
|
||||
@@ -56,7 +56,9 @@ export default function McpAuditTab() {
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
void fetchStats();
|
||||
void (async () => {
|
||||
await fetchStats();
|
||||
})();
|
||||
}, [fetchStats]);
|
||||
|
||||
const fetchAudit = useCallback(async () => {
|
||||
@@ -86,7 +88,9 @@ export default function McpAuditTab() {
|
||||
}, [offset, successFilter, t, toolFilter]);
|
||||
|
||||
useEffect(() => {
|
||||
void fetchAudit();
|
||||
void (async () => {
|
||||
await fetchAudit();
|
||||
})();
|
||||
}, [fetchAudit]);
|
||||
|
||||
return (
|
||||
|
||||
@@ -63,7 +63,9 @@ export default function CacheEntriesTab() {
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
fetchEntries();
|
||||
void (async () => {
|
||||
await fetchEntries();
|
||||
})();
|
||||
}, [fetchEntries]);
|
||||
|
||||
const handleDelete = async (signature: string) => {
|
||||
|
||||
@@ -130,9 +130,12 @@ export default function ReasoningCacheTab() {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [clearing, setClearing] = useState(false);
|
||||
const [expandedId, setExpandedId] = useState<string | null>(null);
|
||||
// Snapshot of "now" taken when the data lands (never during render — the
|
||||
// purity rule bars Date.now() there); entries only render after a fetch.
|
||||
const [nowMs, setNowMs] = useState(0);
|
||||
|
||||
const timeAgo = (dateStr: string): string => {
|
||||
const diff = Date.now() - new Date(dateStr).getTime();
|
||||
const diff = nowMs - new Date(dateStr).getTime();
|
||||
const minutes = Math.floor(diff / 60000);
|
||||
if (minutes < 1) return t("justNow");
|
||||
if (minutes < 60) return t("minutesAgo", { minutes });
|
||||
@@ -147,6 +150,7 @@ export default function ReasoningCacheTab() {
|
||||
const res = await fetch("/api/cache/reasoning");
|
||||
if (res.ok) {
|
||||
const json: ReasoningCacheData = await res.json();
|
||||
setNowMs(Date.now());
|
||||
setData(json);
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -157,7 +161,9 @@ export default function ReasoningCacheTab() {
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
void fetchData();
|
||||
void (async () => {
|
||||
await fetchData();
|
||||
})();
|
||||
const id = setInterval(() => void fetchData(), REFRESH_INTERVAL_MS);
|
||||
return () => clearInterval(id);
|
||||
}, [fetchData]);
|
||||
|
||||
4
src/app/(dashboard)/dashboard/cache/page.tsx
vendored
4
src/app/(dashboard)/dashboard/cache/page.tsx
vendored
@@ -374,7 +374,9 @@ export default function CachePage() {
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
void fetchStats();
|
||||
void (async () => {
|
||||
await fetchStats();
|
||||
})();
|
||||
const id = setInterval(() => void fetchStats(), REFRESH_INTERVAL_MS);
|
||||
return () => clearInterval(id);
|
||||
}, [fetchStats]);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { useState } from "react";
|
||||
import { Modal, Button, Input, Select } from "@/shared/components";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
@@ -29,7 +29,14 @@ export default function EditMemoryModal({ memory, isOpen, onClose, onSaved }: Pr
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
// Adjust-during-render (React docs pattern): when the modal (re)opens for a
|
||||
// memory, seed the form fields from it before painting — no effect round-trip.
|
||||
const [prevSync, setPrevSync] = useState<{ memory: Memory | null; isOpen: boolean }>({
|
||||
memory: null,
|
||||
isOpen: false,
|
||||
});
|
||||
if (memory !== prevSync.memory || isOpen !== prevSync.isOpen) {
|
||||
setPrevSync({ memory, isOpen });
|
||||
if (memory && isOpen) {
|
||||
setType(memory.type);
|
||||
setKey(memory.key);
|
||||
@@ -38,7 +45,7 @@ export default function EditMemoryModal({ memory, isOpen, onClose, onSaved }: Pr
|
||||
setMetadataError("");
|
||||
setError("");
|
||||
}
|
||||
}, [memory, isOpen]);
|
||||
}
|
||||
|
||||
const handleMetadataChange = (value: string) => {
|
||||
setMetadataStr(value);
|
||||
@@ -151,9 +158,7 @@ export default function EditMemoryModal({ memory, isOpen, onClose, onSaved }: Pr
|
||||
metadataError ? "border-red-500" : "border-border"
|
||||
}`}
|
||||
/>
|
||||
{metadataError && (
|
||||
<p className="text-xs text-red-400 mt-1">{metadataError}</p>
|
||||
)}
|
||||
{metadataError && <p className="text-xs text-red-400 mt-1">{metadataError}</p>}
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
@@ -40,7 +40,8 @@ export default function QdrantConfigCard() {
|
||||
collection?: { exists: boolean; vectorSize?: number; vectorName?: string | null };
|
||||
} | null>(null);
|
||||
const [searchValidated, setSearchValidated] = useState(false);
|
||||
const [tutorialOpen, setTutorialOpen] = useState(false); const [checking, setChecking] = useState(false);
|
||||
const [tutorialOpen, setTutorialOpen] = useState(false);
|
||||
const [checking, setChecking] = useState(false);
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [searching, setSearching] = useState(false);
|
||||
const [searchResults, setSearchResults] = useState<
|
||||
@@ -109,7 +110,8 @@ export default function QdrantConfigCard() {
|
||||
// invalidate in-flight checks so they cannot overwrite the new state.
|
||||
healthSeqRef.current += 1;
|
||||
setHealth(null);
|
||||
setSearchValidated(false); setQdrant(next);
|
||||
setSearchValidated(false);
|
||||
setQdrant(next);
|
||||
setSaving(true);
|
||||
setSaveStatus("");
|
||||
try {
|
||||
@@ -153,7 +155,8 @@ export default function QdrantConfigCard() {
|
||||
setSaving(false);
|
||||
}
|
||||
},
|
||||
[qdrant, checkHealth] );
|
||||
[qdrant, checkHealth]
|
||||
);
|
||||
|
||||
// Auto-check on mount once settings load: without this the status badge
|
||||
// renders red after a page refresh because `health` starts as null and the
|
||||
@@ -161,7 +164,9 @@ export default function QdrantConfigCard() {
|
||||
// connection button still drives the same check manually.
|
||||
useEffect(() => {
|
||||
if (!loading && qdrant.enabled && health === null) {
|
||||
void checkHealth();
|
||||
void (async () => {
|
||||
await checkHealth();
|
||||
})();
|
||||
}
|
||||
}, [loading, qdrant.enabled, health, checkHealth]);
|
||||
|
||||
@@ -245,7 +250,8 @@ export default function QdrantConfigCard() {
|
||||
? "text-text-muted"
|
||||
: health.ok
|
||||
? "text-emerald-500"
|
||||
: "text-red-500" }`}
|
||||
: "text-red-500"
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`inline-block w-2.5 h-2.5 rounded-full ${
|
||||
|
||||
@@ -190,9 +190,7 @@ export default function MemoriesTab() {
|
||||
else skipped++;
|
||||
}
|
||||
fetchMemories();
|
||||
setImportStatus(
|
||||
t("importResult", { imported, skipped }),
|
||||
);
|
||||
setImportStatus(t("importResult", { imported, skipped }));
|
||||
} catch {
|
||||
setImportStatus(t("importError"));
|
||||
} finally {
|
||||
@@ -239,7 +237,9 @@ export default function MemoriesTab() {
|
||||
// Auto-run health check on mount + poll every 30s, so the indicator reflects
|
||||
// engine health without requiring a manual click.
|
||||
useEffect(() => {
|
||||
void checkHealth();
|
||||
void (async () => {
|
||||
await checkHealth();
|
||||
})();
|
||||
const id = setInterval(() => {
|
||||
void checkHealth();
|
||||
}, 30_000);
|
||||
@@ -260,8 +260,9 @@ export default function MemoriesTab() {
|
||||
body: JSON.stringify({ dryRun: true, olderThanDays: 30 }),
|
||||
});
|
||||
const data = await res.json().catch(() => null);
|
||||
const candidates: string[] =
|
||||
Array.isArray(data?.candidates) ? data.candidates.map((c: { key?: string }) => c?.key ?? String(c)) : [];
|
||||
const candidates: string[] = Array.isArray(data?.candidates)
|
||||
? data.candidates.map((c: { key?: string }) => c?.key ?? String(c))
|
||||
: [];
|
||||
setSummarizeCandidates(candidates);
|
||||
setSummarizeDialogOpen(true);
|
||||
} catch {
|
||||
@@ -289,8 +290,7 @@ export default function MemoriesTab() {
|
||||
}
|
||||
};
|
||||
|
||||
const showHitRate =
|
||||
(stats.cacheStats?.hits ?? 0) + (stats.cacheStats?.misses ?? 0) > 0;
|
||||
const showHitRate = (stats.cacheStats?.hits ?? 0) + (stats.cacheStats?.misses ?? 0) > 0;
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
@@ -401,9 +401,7 @@ export default function MemoriesTab() {
|
||||
info
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-2xl font-bold">
|
||||
{((stats.hitRate ?? 0) * 100).toFixed(1)}%
|
||||
</div>
|
||||
<div className="text-2xl font-bold">{((stats.hitRate ?? 0) * 100).toFixed(1)}%</div>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
@@ -448,12 +446,8 @@ export default function MemoriesTab() {
|
||||
<span className="material-symbols-outlined text-[40px] text-text-muted mb-3">
|
||||
psychology
|
||||
</span>
|
||||
<p className="text-sm font-medium text-text-main mb-1">
|
||||
{t("emptyState.title")}
|
||||
</p>
|
||||
<p className="text-xs text-text-muted max-w-xs">
|
||||
{t("emptyState.description")}
|
||||
</p>
|
||||
<p className="text-sm font-medium text-text-main mb-1">{t("emptyState.title")}</p>
|
||||
<p className="text-xs text-text-muted max-w-xs">{t("emptyState.description")}</p>
|
||||
<Button className="mt-4" size="sm" onClick={() => setAddDialogOpen(true)}>
|
||||
{t("addMemory")}
|
||||
</Button>
|
||||
@@ -477,7 +471,9 @@ export default function MemoriesTab() {
|
||||
<td className="py-2 px-4">
|
||||
<Badge
|
||||
variant={getTypeColor(memory.type)}
|
||||
title={t(TYPE_TOOLTIPS[memory.type]?.replace("memory.", "") ?? memory.type)}
|
||||
title={t(
|
||||
TYPE_TOOLTIPS[memory.type]?.replace("memory.", "") ?? memory.type
|
||||
)}
|
||||
>
|
||||
{t(memory.type)}
|
||||
</Badge>
|
||||
@@ -665,7 +661,10 @@ export default function MemoriesTab() {
|
||||
</p>
|
||||
<ul className="space-y-1 max-h-48 overflow-y-auto">
|
||||
{summarizeCandidates.map((key, i) => (
|
||||
<li key={i} className="text-xs font-mono text-text-main truncate px-2 py-1 bg-surface/30 rounded">
|
||||
<li
|
||||
key={i}
|
||||
className="text-xs font-mono text-text-main truncate px-2 py-1 bg-surface/30 rounded"
|
||||
>
|
||||
{key}
|
||||
</li>
|
||||
))}
|
||||
|
||||
@@ -39,7 +39,9 @@ export function useEngineStatus(refreshIntervalMs = 5000): UseEngineStatusResult
|
||||
|
||||
useEffect(() => {
|
||||
mounted.current = true;
|
||||
void fetchOnce();
|
||||
void (async () => {
|
||||
await fetchOnce();
|
||||
})();
|
||||
if (!refreshIntervalMs || refreshIntervalMs <= 0) {
|
||||
return () => {
|
||||
mounted.current = false;
|
||||
|
||||
@@ -40,7 +40,9 @@ export function useMemorySettings(): UseMemorySettingsResult {
|
||||
|
||||
useEffect(() => {
|
||||
mounted.current = true;
|
||||
void fetchOnce();
|
||||
void (async () => {
|
||||
await fetchOnce();
|
||||
})();
|
||||
return () => {
|
||||
mounted.current = false;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
"use client";
|
||||
/** Polls the 3 agent sources (allSettled), listens to the `requests` WS channel as a refetch trigger. */
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useLiveDashboard } from "@/hooks/useLiveDashboard";
|
||||
import type { CloudAgentTask } from "@/lib/cloudAgent/types";
|
||||
import type { A2ATask } from "@/lib/a2a/taskManager";
|
||||
import type { FleetSnapshot } from "@/lib/conductor/hubProxy";
|
||||
import { fromCloudAgent } from "../model/fromCloudAgent";
|
||||
import { fromA2A } from "../model/fromA2A";
|
||||
import { fromConductor } from "../model/fromConductor";
|
||||
import { mergeSnapshot } from "../model/mergeSnapshot";
|
||||
import type { OrchSnapshot, SourceStatus } from "../model/orchestrationTypes";
|
||||
|
||||
export const POLL_MS = 5_000;
|
||||
export const WS_REFETCH_DEBOUNCE_MS = 1_000;
|
||||
|
||||
interface Raw {
|
||||
cloudAgent: CloudAgentTask[];
|
||||
a2a: A2ATask[];
|
||||
conductor: FleetSnapshot;
|
||||
}
|
||||
const EMPTY_RAW: Raw = {
|
||||
cloudAgent: [],
|
||||
a2a: [],
|
||||
conductor: { offline: true, runners: [], tasks: [] },
|
||||
};
|
||||
|
||||
async function fetchJson<T>(url: string, signal: AbortSignal): Promise<T> {
|
||||
const res = await fetch(url, { signal, cache: "no-store" });
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
return res.json() as Promise<T>;
|
||||
}
|
||||
|
||||
/** Builds the 3-source status list from a `Promise.allSettled` triple. */
|
||||
function buildSourceStatuses(
|
||||
ca: PromiseSettledResult<{ data: CloudAgentTask[] }>,
|
||||
a2a: PromiseSettledResult<{ tasks: A2ATask[] }>,
|
||||
cond: PromiseSettledResult<FleetSnapshot>,
|
||||
nowIso: string
|
||||
): SourceStatus[] {
|
||||
const next: SourceStatus[] = [];
|
||||
if (ca.status === "fulfilled") next.push({ source: "cloud-agent", ok: true });
|
||||
else
|
||||
next.push({
|
||||
source: "cloud-agent",
|
||||
ok: false,
|
||||
error: String(ca.reason),
|
||||
staleSince: nowIso,
|
||||
});
|
||||
if (a2a.status === "fulfilled") next.push({ source: "a2a", ok: true });
|
||||
else next.push({ source: "a2a", ok: false, error: String(a2a.reason), staleSince: nowIso });
|
||||
if (cond.status === "fulfilled") {
|
||||
next.push({ source: "conductor", ok: true, offline: cond.value.offline });
|
||||
} else
|
||||
next.push({
|
||||
source: "conductor",
|
||||
ok: false,
|
||||
error: String(cond.reason),
|
||||
staleSince: nowIso,
|
||||
});
|
||||
return next;
|
||||
}
|
||||
|
||||
export function useOrchestrationSnapshot() {
|
||||
// `raw` and `polledAt` are React state (not refs) so the merge below reads them
|
||||
// during render like any other state — a ref read during render trips the
|
||||
// `react-hooks/refs` lint rule, and computing `Date.now()` inline in the memo
|
||||
// factory trips `react-hooks/purity`. Sampling `Date.now()` once per poll (inside
|
||||
// the effect, not during render) keeps `mergeSnapshot`'s staleness math correct
|
||||
// without either violation.
|
||||
const [raw, setRaw] = useState<Raw>(EMPTY_RAW);
|
||||
const [statuses, setStatuses] = useState<SourceStatus[]>([]);
|
||||
// Lazy initializer (not a literal 0) so the pre-first-poll render already has a
|
||||
// real timestamp — with `0` the very first `mergeSnapshot` call stamped
|
||||
// `generatedAt` as the 1970 epoch. Safe: with EMPTY_RAW there is nothing to
|
||||
// staleness-filter at mount, so seeding `Date.now()` here changes no behavior.
|
||||
const [polledAt, setPolledAt] = useState<number>(() => Date.now());
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [showCompleted, setShowCompleted] = useState(false);
|
||||
const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
// Populated by the mount effect below; lets `refetch()` (and the WS debounce
|
||||
// trigger) reach the same poll loop without hoisting it out of the effect —
|
||||
// hoisting to a top-level `useCallback` invoked from the effect body trips
|
||||
// `react-hooks/set-state-in-effect`.
|
||||
const pollRef = useRef<() => void>(() => {});
|
||||
|
||||
useEffect(() => {
|
||||
const controller = new AbortController();
|
||||
|
||||
const poll = async () => {
|
||||
const [ca, a2a, cond] = await Promise.allSettled([
|
||||
fetchJson<{ data: CloudAgentTask[] }>("/api/v1/agents/tasks?limit=100", controller.signal),
|
||||
fetchJson<{ tasks: A2ATask[] }>("/api/a2a/tasks?limit=200", controller.signal),
|
||||
fetchJson<FleetSnapshot>("/api/conductor/fleet", controller.signal),
|
||||
]);
|
||||
if (controller.signal.aborted) return;
|
||||
const nowMs = Date.now();
|
||||
const nowIso = new Date(nowMs).toISOString();
|
||||
const next = buildSourceStatuses(ca, a2a, cond, nowIso);
|
||||
|
||||
// Failed sources keep the previously stored slice — only overwrite what
|
||||
// actually resolved this round ("last good data" contract from the brief).
|
||||
setRaw((prev) => ({
|
||||
cloudAgent: ca.status === "fulfilled" ? ca.value.data : prev.cloudAgent,
|
||||
a2a: a2a.status === "fulfilled" ? a2a.value.tasks : prev.a2a,
|
||||
conductor: cond.status === "fulfilled" ? cond.value : prev.conductor,
|
||||
}));
|
||||
setStatuses(next);
|
||||
setPolledAt(nowMs);
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
pollRef.current = () => void poll();
|
||||
void poll();
|
||||
const id = setInterval(() => void poll(), POLL_MS);
|
||||
return () => {
|
||||
clearInterval(id);
|
||||
controller.abort();
|
||||
if (debounceRef.current) {
|
||||
clearTimeout(debounceRef.current);
|
||||
debounceRef.current = null;
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const refetch = useCallback(() => {
|
||||
pollRef.current();
|
||||
}, []);
|
||||
|
||||
useLiveDashboard({
|
||||
channels: ["requests"],
|
||||
onEvent: (payload) => {
|
||||
if (payload.channel !== "requests") return;
|
||||
if (debounceRef.current) return; // debounce burst → one refetch
|
||||
debounceRef.current = setTimeout(() => {
|
||||
debounceRef.current = null;
|
||||
refetch();
|
||||
}, WS_REFETCH_DEBOUNCE_MS);
|
||||
},
|
||||
});
|
||||
|
||||
const snapshot: OrchSnapshot = useMemo(
|
||||
() =>
|
||||
mergeSnapshot(
|
||||
{
|
||||
cloudAgent: fromCloudAgent(raw.cloudAgent),
|
||||
a2a: fromA2A(raw.a2a),
|
||||
conductor: fromConductor(raw.conductor),
|
||||
},
|
||||
statuses,
|
||||
{ now: polledAt, showCompleted }
|
||||
),
|
||||
[raw, statuses, showCompleted, polledAt]
|
||||
);
|
||||
|
||||
return { snapshot, isLoading, showCompleted, setShowCompleted, refetch };
|
||||
}
|
||||
53
src/app/(dashboard)/dashboard/orchestration/model/fromA2A.ts
Normal file
53
src/app/(dashboard)/dashboard/orchestration/model/fromA2A.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
/** A2A tasks → unified orchestration nodes. Pure. */
|
||||
import type { A2ATask } from "@/lib/a2a/taskManager";
|
||||
import type { OrchEdge, OrchNode, OrchState } from "./orchestrationTypes";
|
||||
|
||||
const STATE_MAP: Record<string, OrchState> = {
|
||||
submitted: "queued",
|
||||
working: "running",
|
||||
completed: "succeeded",
|
||||
failed: "failed",
|
||||
cancelled: "cancelled",
|
||||
};
|
||||
const TERMINAL: ReadonlySet<OrchState> = new Set(["succeeded", "failed", "cancelled"]);
|
||||
|
||||
function truncate(s: string, n = 60): string {
|
||||
return s.length > n ? `${s.slice(0, n - 1)}…` : s;
|
||||
}
|
||||
|
||||
export function fromA2A(tasks: A2ATask[]): { nodes: OrchNode[]; edges: OrchEdge[] } {
|
||||
if (tasks.length === 0) return { nodes: [], edges: [] };
|
||||
const nodes: OrchNode[] = [];
|
||||
const edges: OrchEdge[] = [];
|
||||
const counts: Partial<Record<OrchState, number>> = {};
|
||||
|
||||
for (const t of tasks) {
|
||||
const mapped = STATE_MAP[t.state];
|
||||
const state: OrchState = mapped ?? "failed";
|
||||
counts[state] = (counts[state] ?? 0) + 1;
|
||||
const id = `a2a:${t.id}`;
|
||||
const firstUser = t.input.messages.find((m) => m.role === "user")?.content ?? "";
|
||||
nodes.push({
|
||||
id,
|
||||
kind: "work",
|
||||
source: "a2a",
|
||||
state,
|
||||
label: t.skill,
|
||||
sublabel: mapped ? truncate(firstUser) : `unknown state: ${String(t.state)}`,
|
||||
startedAt: t.createdAt,
|
||||
updatedAt: t.updatedAt,
|
||||
endedAt: TERMINAL.has(state) ? t.updatedAt : undefined,
|
||||
raw: t,
|
||||
});
|
||||
edges.push({
|
||||
id: `e:source:a2a→${id}`,
|
||||
from: "source:a2a",
|
||||
to: id,
|
||||
kind: "owns",
|
||||
active: state === "running",
|
||||
});
|
||||
}
|
||||
|
||||
nodes.unshift({ id: "source:a2a", kind: "source", source: "a2a", label: "A2A", counts });
|
||||
return { nodes, edges };
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
/** Cloud Agent tasks → unified orchestration nodes. Pure. */
|
||||
import type { CloudAgentTask } from "@/lib/cloudAgent/types";
|
||||
import type { OrchEdge, OrchNode, OrchState } from "./orchestrationTypes";
|
||||
|
||||
const STATE_MAP: Record<string, OrchState> = {
|
||||
queued: "queued",
|
||||
running: "running",
|
||||
awaiting_approval: "waiting_approval",
|
||||
completed: "succeeded",
|
||||
failed: "failed",
|
||||
cancelled: "cancelled",
|
||||
};
|
||||
|
||||
function truncate(s: string, n = 60): string {
|
||||
return s.length > n ? `${s.slice(0, n - 1)}…` : s;
|
||||
}
|
||||
|
||||
export function fromCloudAgent(tasks: CloudAgentTask[]): { nodes: OrchNode[]; edges: OrchEdge[] } {
|
||||
if (tasks.length === 0) return { nodes: [], edges: [] };
|
||||
const nodes: OrchNode[] = [];
|
||||
const edges: OrchEdge[] = [];
|
||||
const counts: Partial<Record<OrchState, number>> = {};
|
||||
|
||||
for (const t of tasks) {
|
||||
const mapped = STATE_MAP[t.status];
|
||||
const state: OrchState = mapped ?? "failed";
|
||||
counts[state] = (counts[state] ?? 0) + 1;
|
||||
const id = `cloud-agent:${t.id}`;
|
||||
const active = state === "running";
|
||||
nodes.push({
|
||||
id,
|
||||
kind: "work",
|
||||
source: "cloud-agent",
|
||||
state,
|
||||
label: truncate(t.prompt),
|
||||
sublabel: mapped ? t.providerId : `${t.providerId} — unknown status: ${String(t.status)}`,
|
||||
startedAt: t.createdAt,
|
||||
updatedAt: t.updatedAt,
|
||||
endedAt: t.completedAt,
|
||||
cost: t.result?.cost,
|
||||
raw: t,
|
||||
});
|
||||
edges.push({
|
||||
id: `e:source:cloud-agent→${id}`,
|
||||
from: "source:cloud-agent",
|
||||
to: id,
|
||||
kind: "owns",
|
||||
active,
|
||||
});
|
||||
|
||||
const last = t.activities[t.activities.length - 1];
|
||||
if (active && last) {
|
||||
const actId = `${id}:activity`;
|
||||
nodes.push({
|
||||
id: actId,
|
||||
kind: "activity",
|
||||
source: "cloud-agent",
|
||||
state,
|
||||
label: truncate(last.content),
|
||||
sublabel: last.type,
|
||||
updatedAt: last.timestamp,
|
||||
});
|
||||
edges.push({ id: `e:${id}→${actId}`, from: id, to: actId, kind: "owns", active: true });
|
||||
}
|
||||
}
|
||||
|
||||
nodes.unshift({
|
||||
id: "source:cloud-agent",
|
||||
kind: "source",
|
||||
source: "cloud-agent",
|
||||
label: "Cloud Agent",
|
||||
counts,
|
||||
});
|
||||
return { nodes, edges };
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
/** Conductor fleet snapshot → unified orchestration nodes. Pure. */
|
||||
import type { FleetRunner, FleetSnapshot, FleetTask } from "@/lib/conductor/hubProxy";
|
||||
import type { OrchEdge, OrchNode, OrchState } from "./orchestrationTypes";
|
||||
|
||||
const TERMINAL: ReadonlySet<OrchState> = new Set(["succeeded", "failed", "cancelled"]);
|
||||
|
||||
function mapHubStatus(status: string): OrchState | null {
|
||||
const s = status.toLowerCase();
|
||||
if (s === "queued" || s === "pending") return "queued";
|
||||
if (s === "running" || s === "working" || s === "scheduled") return "running";
|
||||
if (s === "done" || s === "completed" || s === "succeeded") return "succeeded";
|
||||
if (s === "failed" || s === "error") return "failed";
|
||||
if (s === "cancelled" || s === "canceled") return "cancelled";
|
||||
return null;
|
||||
}
|
||||
|
||||
function taskNode(t: FleetTask, kind: "work" | "activity"): OrchNode {
|
||||
const mapped = mapHubStatus(t.status);
|
||||
const state: OrchState = mapped ?? "failed";
|
||||
return {
|
||||
id: `conductor:task:${t.id}`,
|
||||
kind,
|
||||
source: "conductor",
|
||||
state,
|
||||
label: t.summary ?? t.id,
|
||||
sublabel: mapped ? (t.repo ?? t.mode) : `unknown status: ${t.status}`,
|
||||
updatedAt: t.updated_at ?? undefined,
|
||||
// FleetTask has no dedicated completion timestamp — updated_at is the closest
|
||||
// proxy, same pattern as fromA2A.ts (A2ATask has no completedAt either).
|
||||
endedAt: TERMINAL.has(state) ? (t.updated_at ?? undefined) : undefined,
|
||||
raw: t,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Tasks whose runner actually exists in `snap.runners` AND is currently "running" — those
|
||||
* get "absorbed" into that runner's ActivityNode instead of getting their own work node.
|
||||
* A running task pointing at a runner id that has since deregistered falls through to the
|
||||
* normal work-node loop instead of being silently skipped as "already an activity".
|
||||
*/
|
||||
function computeActiveByRunner(snap: FleetSnapshot): Map<string, FleetTask> {
|
||||
const runnerIds = new Set(snap.runners.map((r) => r.id));
|
||||
const activeByRunner = new Map<string, FleetTask>();
|
||||
for (const t of snap.tasks) {
|
||||
if (t.runner && runnerIds.has(t.runner) && mapHubStatus(t.status) === "running") {
|
||||
activeByRunner.set(t.runner, t);
|
||||
}
|
||||
}
|
||||
return activeByRunner;
|
||||
}
|
||||
|
||||
function runnerState(r: FleetRunner, activeTask: FleetTask | undefined): OrchState {
|
||||
if (!r.online) return "failed";
|
||||
if (r.draining) return "cancelled";
|
||||
return activeTask ? "running" : "queued";
|
||||
}
|
||||
|
||||
/** One work node per runner, plus an activity node for its currently-active task. */
|
||||
function runnerWorkNodes(
|
||||
snap: FleetSnapshot,
|
||||
activeByRunner: Map<string, FleetTask>,
|
||||
bump: (s: OrchState) => void
|
||||
): { nodes: OrchNode[]; edges: OrchEdge[] } {
|
||||
const nodes: OrchNode[] = [];
|
||||
const edges: OrchEdge[] = [];
|
||||
for (const r of snap.runners) {
|
||||
const id = `conductor:runner:${r.id}`;
|
||||
const activeTask = activeByRunner.get(r.id);
|
||||
const state = runnerState(r, activeTask);
|
||||
bump(state);
|
||||
nodes.push({
|
||||
id,
|
||||
kind: "work",
|
||||
source: "conductor",
|
||||
state,
|
||||
label: r.name,
|
||||
sublabel: r.clis.join(", "),
|
||||
raw: r,
|
||||
});
|
||||
edges.push({
|
||||
id: `e:source:conductor→${id}`,
|
||||
from: "source:conductor",
|
||||
to: id,
|
||||
kind: "owns",
|
||||
active: state === "running",
|
||||
});
|
||||
if (activeTask) {
|
||||
nodes.push(taskNode(activeTask, "activity"));
|
||||
edges.push({
|
||||
id: `e:${id}→conductor:task:${activeTask.id}`,
|
||||
from: id,
|
||||
to: `conductor:task:${activeTask.id}`,
|
||||
kind: "owns",
|
||||
active: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
return { nodes, edges };
|
||||
}
|
||||
|
||||
/** Work nodes for tasks not already absorbed as a runner's activity node. */
|
||||
function remainingTaskWorkNodes(
|
||||
snap: FleetSnapshot,
|
||||
activeByRunner: Map<string, FleetTask>,
|
||||
bump: (s: OrchState) => void
|
||||
): { nodes: OrchNode[]; edges: OrchEdge[] } {
|
||||
const nodes: OrchNode[] = [];
|
||||
const edges: OrchEdge[] = [];
|
||||
for (const t of snap.tasks) {
|
||||
if (t.runner && activeByRunner.get(t.runner)?.id === t.id) continue; // already an activity
|
||||
const node = taskNode(t, "work");
|
||||
bump(node.state as OrchState);
|
||||
nodes.push(node);
|
||||
edges.push({
|
||||
id: `e:source:conductor→${node.id}`,
|
||||
from: "source:conductor",
|
||||
to: node.id,
|
||||
kind: "owns",
|
||||
active: node.state === "running",
|
||||
});
|
||||
}
|
||||
return { nodes, edges };
|
||||
}
|
||||
|
||||
export function fromConductor(snap: FleetSnapshot): { nodes: OrchNode[]; edges: OrchEdge[] } {
|
||||
if (snap.offline || (snap.runners.length === 0 && snap.tasks.length === 0)) {
|
||||
return { nodes: [], edges: [] };
|
||||
}
|
||||
const counts: Partial<Record<OrchState, number>> = {};
|
||||
const bump = (s: OrchState) => {
|
||||
counts[s] = (counts[s] ?? 0) + 1;
|
||||
};
|
||||
|
||||
const activeByRunner = computeActiveByRunner(snap);
|
||||
const runners = runnerWorkNodes(snap, activeByRunner, bump);
|
||||
const tasks = remainingTaskWorkNodes(snap, activeByRunner, bump);
|
||||
const nodes = [...runners.nodes, ...tasks.nodes];
|
||||
const edges = [...runners.edges, ...tasks.edges];
|
||||
|
||||
nodes.unshift({
|
||||
id: "source:conductor",
|
||||
kind: "source",
|
||||
source: "conductor",
|
||||
label: "Conductor",
|
||||
counts,
|
||||
});
|
||||
return { nodes, edges };
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
/** Merge the three source mappers into one snapshot: root, dedupe, staleness filter, cap. Pure. */
|
||||
import {
|
||||
MAX_WORK_NODES,
|
||||
STALE_COMPLETED_MS,
|
||||
type OrchEdge,
|
||||
type OrchNode,
|
||||
type OrchSnapshot,
|
||||
type OrchSource,
|
||||
type OrchState,
|
||||
type SourceStatus,
|
||||
} from "./orchestrationTypes";
|
||||
|
||||
const TERMINAL: ReadonlySet<OrchState> = new Set(["succeeded", "failed", "cancelled"]);
|
||||
|
||||
export interface MergeOptions {
|
||||
now: number;
|
||||
showCompleted?: boolean;
|
||||
}
|
||||
|
||||
interface Part {
|
||||
nodes: OrchNode[];
|
||||
edges: OrchEdge[];
|
||||
}
|
||||
|
||||
interface NodesAndEdges {
|
||||
nodes: OrchNode[];
|
||||
edges: OrchEdge[];
|
||||
}
|
||||
|
||||
function conductorMirrorId(node: OrchNode): string | null {
|
||||
const raw = node.raw as { metadata?: { conductor?: { task_id?: unknown } } } | undefined;
|
||||
const id = raw?.metadata?.conductor?.task_id;
|
||||
return typeof id === "string" ? id : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* (2) Conductor↔A2A dedupe — key verified in src/lib/conductor/bridge.ts::ensureMirrored.
|
||||
* Mutates `dropped` in place; returns the (possibly patched) nodes/edges.
|
||||
*/
|
||||
function dedupeConductorMirrors(
|
||||
nodes: OrchNode[],
|
||||
edges: OrchEdge[],
|
||||
dropped: Set<string>
|
||||
): NodesAndEdges {
|
||||
const conductorTaskIds = new Set(
|
||||
nodes
|
||||
.filter((n) => n.source === "conductor" && n.id.startsWith("conductor:task:"))
|
||||
.map((n) => n.id.slice("conductor:task:".length))
|
||||
);
|
||||
const nextNodes = [...nodes];
|
||||
const nextEdges = [...edges];
|
||||
for (const n of nextNodes) {
|
||||
if (n.source !== "a2a" || n.kind !== "work") continue;
|
||||
const mirror = conductorMirrorId(n);
|
||||
if (mirror && conductorTaskIds.has(mirror)) {
|
||||
dropped.add(n.id);
|
||||
const cIndex = nextNodes.findIndex((c) => c.id === `conductor:task:${mirror}`);
|
||||
if (cIndex !== -1) {
|
||||
// Copy rather than mutate — the original object is still referenced by
|
||||
// parts.conductor.nodes, and this function's contract is Pure.
|
||||
const cNode: OrchNode = { ...nextNodes[cIndex], mirrorOf: n.id };
|
||||
nextNodes[cIndex] = cNode;
|
||||
nextEdges.push({
|
||||
id: `e:mirror:${cNode.id}`,
|
||||
from: cNode.id,
|
||||
to: "source:a2a",
|
||||
kind: "mirror",
|
||||
active: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return { nodes: nextNodes, edges: nextEdges };
|
||||
}
|
||||
|
||||
/** (3) staleness filter — adds stale terminal work/activity node ids to `dropped`. */
|
||||
function markStaleCompleted(nodes: OrchNode[], now: number, dropped: Set<string>): void {
|
||||
for (const n of nodes) {
|
||||
if (n.kind !== "work" && n.kind !== "activity") continue;
|
||||
if (
|
||||
n.state &&
|
||||
TERMINAL.has(n.state) &&
|
||||
n.endedAt &&
|
||||
now - Date.parse(n.endedAt) > STALE_COMPLETED_MS
|
||||
) {
|
||||
dropped.add(n.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** One source's overflow placeholder node, or null when it fits under `budgetPer`. */
|
||||
function overflowNodeForSource(
|
||||
source: OrchSource,
|
||||
list: OrchNode[],
|
||||
budgetPer: number,
|
||||
dropped: Set<string>
|
||||
): OrchNode | null {
|
||||
if (list.length <= budgetPer) return null;
|
||||
list.sort((a, b) => Date.parse(b.updatedAt ?? "0") - Date.parse(a.updatedAt ?? "0"));
|
||||
const excess = list.slice(budgetPer);
|
||||
const counts: Partial<Record<OrchState, number>> = {};
|
||||
for (const n of excess) {
|
||||
dropped.add(n.id);
|
||||
if (n.state) counts[n.state] = (counts[n.state] ?? 0) + 1;
|
||||
}
|
||||
return {
|
||||
id: `overflow:${source}`,
|
||||
kind: "overflow",
|
||||
source,
|
||||
label: `+${excess.length} more`,
|
||||
counts,
|
||||
// Additive: lets overviewProjection fold true per-state totals into its
|
||||
// counters even though these nodes no longer render on the canvas
|
||||
// (operator ruling — spec governs, counters must show TRUE totals).
|
||||
droppedByState: counts,
|
||||
};
|
||||
}
|
||||
|
||||
/** (4) cap with per-source overflow, newest kept. */
|
||||
function capWorkNodesWithOverflow(
|
||||
nodes: OrchNode[],
|
||||
edges: OrchEdge[],
|
||||
dropped: Set<string>
|
||||
): NodesAndEdges {
|
||||
const works = nodes.filter((n) => n.kind === "work");
|
||||
if (works.length <= MAX_WORK_NODES) return { nodes, edges };
|
||||
|
||||
const bySource = new Map<OrchSource, OrchNode[]>();
|
||||
for (const w of works) {
|
||||
const list = bySource.get(w.source as OrchSource) ?? [];
|
||||
list.push(w);
|
||||
bySource.set(w.source as OrchSource, list);
|
||||
}
|
||||
const budgetPer = Math.max(1, Math.floor(MAX_WORK_NODES / bySource.size));
|
||||
const overflowNodes: OrchNode[] = [];
|
||||
for (const [source, list] of bySource) {
|
||||
const overflow = overflowNodeForSource(source, list, budgetPer, dropped);
|
||||
if (overflow) overflowNodes.push(overflow);
|
||||
}
|
||||
|
||||
const nextNodes = nodes.filter((n) => !dropped.has(n.id)).concat(overflowNodes);
|
||||
const nextEdges = edges.filter((e) => !dropped.has(e.from) && !dropped.has(e.to));
|
||||
for (const o of overflowNodes) {
|
||||
nextEdges.push({
|
||||
id: `e:source:${o.source}→${o.id}`,
|
||||
from: `source:${o.source}`,
|
||||
to: o.id,
|
||||
kind: "owns",
|
||||
active: false,
|
||||
});
|
||||
}
|
||||
return { nodes: nextNodes, edges: nextEdges };
|
||||
}
|
||||
|
||||
/**
|
||||
* (1) root — link every present SourceNode, plus failed sources so the UI can show them
|
||||
* stale. Returns nodes with the root prepended.
|
||||
*/
|
||||
function buildRootAndSourceEdges(
|
||||
nodes: OrchNode[],
|
||||
edges: OrchEdge[],
|
||||
sources: SourceStatus[]
|
||||
): NodesAndEdges {
|
||||
const root: OrchNode = { id: "orchestrator", kind: "orchestrator", label: "OmniRoute" };
|
||||
const nextNodes = [...nodes];
|
||||
const nextEdges = [...edges];
|
||||
const sourceIds = new Set(nextNodes.filter((n) => n.kind === "source").map((n) => n.id));
|
||||
for (const s of sources) {
|
||||
// `!s.ok` covers hard failures; `s.offline` also materializes a placeholder
|
||||
// for a source that reported ok:true but offline:true (e.g. Conductor with
|
||||
// no hub configured) — otherwise that source never gets a SourceNode at all
|
||||
// and its "offline" sublabel can never render.
|
||||
if ((!s.ok || s.offline) && !sourceIds.has(`source:${s.source}`) && s.source !== "routing") {
|
||||
nextNodes.push({
|
||||
id: `source:${s.source}`,
|
||||
kind: "source",
|
||||
source: s.source,
|
||||
label: s.source,
|
||||
sublabel: s.offline ? "offline" : "error",
|
||||
});
|
||||
sourceIds.add(`source:${s.source}`);
|
||||
}
|
||||
}
|
||||
for (const id of sourceIds) {
|
||||
nextEdges.push({
|
||||
id: `e:orchestrator→${id}`,
|
||||
from: "orchestrator",
|
||||
to: id,
|
||||
kind: "owns",
|
||||
active: false,
|
||||
});
|
||||
}
|
||||
return { nodes: [root, ...nextNodes], edges: nextEdges };
|
||||
}
|
||||
|
||||
export function mergeSnapshot(
|
||||
parts: { cloudAgent: Part; a2a: Part; conductor: Part },
|
||||
sources: SourceStatus[],
|
||||
opts: MergeOptions
|
||||
): OrchSnapshot {
|
||||
let nodes: OrchNode[] = [...parts.cloudAgent.nodes, ...parts.a2a.nodes, ...parts.conductor.nodes];
|
||||
let edges: OrchEdge[] = [...parts.cloudAgent.edges, ...parts.a2a.edges, ...parts.conductor.edges];
|
||||
|
||||
const dropped = new Set<string>();
|
||||
({ nodes, edges } = dedupeConductorMirrors(nodes, edges, dropped));
|
||||
|
||||
if (!opts.showCompleted) {
|
||||
markStaleCompleted(nodes, opts.now, dropped);
|
||||
}
|
||||
nodes = nodes.filter((n) => !dropped.has(n.id));
|
||||
edges = edges.filter((e) => !dropped.has(e.from) && !dropped.has(e.to));
|
||||
|
||||
({ nodes, edges } = capWorkNodesWithOverflow(nodes, edges, dropped));
|
||||
({ nodes, edges } = buildRootAndSourceEdges(nodes, edges, sources));
|
||||
|
||||
return { nodes, edges, sources, generatedAt: new Date(opts.now).toISOString() };
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/** OrchSnapshot → @xyflow nodes/edges with a deterministic shallow 3-layer layout. Pure. */
|
||||
import type { Edge, Node } from "@xyflow/react";
|
||||
import { edgeStyle } from "@/shared/components/flow/edgeStyles";
|
||||
import type { OrchNodeKind, OrchSnapshot } from "./orchestrationTypes";
|
||||
|
||||
const LAYER_Y: Record<OrchNodeKind, number> = {
|
||||
orchestrator: 0,
|
||||
source: 150,
|
||||
work: 320,
|
||||
overflow: 320,
|
||||
activity: 470,
|
||||
};
|
||||
const X_GAP = 260;
|
||||
|
||||
export function orchestrationToFlow(snap: OrchSnapshot): {
|
||||
nodes: Node[];
|
||||
edges: Edge[];
|
||||
fitKey: string;
|
||||
} {
|
||||
const byLayer = new Map<number, string[]>();
|
||||
for (const n of [...snap.nodes].sort((a, b) => a.id.localeCompare(b.id))) {
|
||||
const y = LAYER_Y[n.kind];
|
||||
const ids = byLayer.get(y) ?? [];
|
||||
ids.push(n.id);
|
||||
byLayer.set(y, ids);
|
||||
}
|
||||
const pos = new Map<string, { x: number; y: number }>();
|
||||
for (const [y, ids] of byLayer) {
|
||||
const width = (ids.length - 1) * X_GAP;
|
||||
ids.forEach((id, i) => pos.set(id, { x: i * X_GAP - width / 2, y }));
|
||||
}
|
||||
|
||||
const stateOf = new Map(snap.nodes.map((n) => [n.id, n.state]));
|
||||
const nodes: Node[] = snap.nodes.map((n) => ({
|
||||
id: n.id,
|
||||
type: n.kind,
|
||||
position: pos.get(n.id)!,
|
||||
data: n as unknown as Record<string, unknown>,
|
||||
}));
|
||||
const edges: Edge[] = snap.edges.map((e) => {
|
||||
const target = stateOf.get(e.to);
|
||||
const style = edgeStyle(e.active, false, target === "failed", target === "succeeded");
|
||||
return {
|
||||
id: e.id,
|
||||
source: e.from,
|
||||
target: e.to,
|
||||
animated: e.active,
|
||||
style: e.kind === "mirror" ? { ...style, strokeDasharray: "6 4" } : style,
|
||||
};
|
||||
});
|
||||
|
||||
const fitKey = snap.nodes
|
||||
.filter((n) => n.kind === "work")
|
||||
.map((n) => n.id)
|
||||
.sort()
|
||||
.join("|");
|
||||
return { nodes, edges, fitKey };
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* Pure domain vocabulary for the Orchestration Canvas — no React, no side effects.
|
||||
* Spec: _tasks/superpowers/specs/2026-08-30-orchestration-canvas-design.md
|
||||
*/
|
||||
import { STATUS_HEX } from "@/shared/constants/statusColors";
|
||||
|
||||
export type OrchState =
|
||||
"queued" | "running" | "waiting_approval" | "succeeded" | "failed" | "cancelled";
|
||||
export type OrchSource = "cloud-agent" | "a2a" | "conductor" | "routing";
|
||||
export type OrchNodeKind = "orchestrator" | "source" | "work" | "activity" | "overflow";
|
||||
|
||||
export interface OrchNode {
|
||||
id: string; // `${source}:${sourceId}` for work nodes
|
||||
kind: OrchNodeKind;
|
||||
source?: OrchSource;
|
||||
state?: OrchState;
|
||||
label: string;
|
||||
sublabel?: string;
|
||||
startedAt?: string;
|
||||
updatedAt?: string;
|
||||
endedAt?: string;
|
||||
cost?: number;
|
||||
counts?: Partial<Record<OrchState, number>>;
|
||||
// Overflow nodes only: per-state counts of the work nodes folded into this
|
||||
// overflow node when the MAX_WORK_NODES cap engages. overviewProjection folds
|
||||
// this into its `counts` totals (never into `columns`) so operators still see
|
||||
// TRUE totals even when the canvas caps the rendered node count.
|
||||
droppedByState?: Partial<Record<OrchState, number>>;
|
||||
mirrorOf?: string;
|
||||
raw?: unknown;
|
||||
}
|
||||
|
||||
export interface OrchEdge {
|
||||
id: string;
|
||||
from: string;
|
||||
to: string;
|
||||
kind: "owns" | "mirror";
|
||||
active: boolean; // true while the target work is `running`
|
||||
}
|
||||
|
||||
export interface SourceStatus {
|
||||
source: OrchSource;
|
||||
ok: boolean;
|
||||
offline?: boolean;
|
||||
error?: string;
|
||||
staleSince?: string;
|
||||
}
|
||||
|
||||
export interface OrchSnapshot {
|
||||
nodes: OrchNode[];
|
||||
edges: OrchEdge[];
|
||||
sources: SourceStatus[];
|
||||
generatedAt: string;
|
||||
}
|
||||
|
||||
export const ORCH_STATES = [
|
||||
"queued",
|
||||
"running",
|
||||
"waiting_approval",
|
||||
"succeeded",
|
||||
"failed",
|
||||
"cancelled",
|
||||
] as const satisfies readonly OrchState[];
|
||||
|
||||
const STATE_HEX: Record<OrchState, string> = {
|
||||
queued: STATUS_HEX.muted,
|
||||
running: STATUS_HEX.warning,
|
||||
waiting_approval: STATUS_HEX.approval,
|
||||
succeeded: STATUS_HEX.success,
|
||||
failed: STATUS_HEX.error,
|
||||
cancelled: STATUS_HEX.muted,
|
||||
};
|
||||
|
||||
export function orchStateColor(state: OrchState): string {
|
||||
return STATE_HEX[state];
|
||||
}
|
||||
|
||||
export const STALE_COMPLETED_MS = 600_000; // completed >10 min ago drop out of the live view
|
||||
export const MAX_WORK_NODES = 40; // beyond this, per-source overflow nodes take over
|
||||
@@ -0,0 +1,50 @@
|
||||
/** OrchSnapshot → overview counters + kanban columns. Pure. */
|
||||
import {
|
||||
ORCH_STATES,
|
||||
type OrchNode,
|
||||
type OrchSnapshot,
|
||||
type OrchState,
|
||||
} from "./orchestrationTypes";
|
||||
|
||||
export interface OverviewData {
|
||||
counts: Record<OrchState, number>;
|
||||
columns: {
|
||||
queued: OrchNode[];
|
||||
running: OrchNode[];
|
||||
waiting_approval: OrchNode[];
|
||||
done: OrchNode[];
|
||||
};
|
||||
}
|
||||
|
||||
export function overviewProjection(snap: OrchSnapshot, comboActive: number): OverviewData {
|
||||
const counts = Object.fromEntries(ORCH_STATES.map((s) => [s, 0])) as Record<OrchState, number>;
|
||||
const columns: OverviewData["columns"] = {
|
||||
queued: [],
|
||||
running: [],
|
||||
waiting_approval: [],
|
||||
done: [],
|
||||
};
|
||||
|
||||
for (const n of snap.nodes) {
|
||||
// Overflow nodes (MAX_WORK_NODES cap) fold their dropped work nodes' true
|
||||
// per-state counts into `counts` only — never into `columns`, since those
|
||||
// nodes are not rendered on the canvas. Counters must show TRUE totals
|
||||
// even when the canvas caps the rendered node count (operator ruling).
|
||||
if (n.kind === "overflow" && n.droppedByState) {
|
||||
for (const s of ORCH_STATES) {
|
||||
counts[s] += n.droppedByState[s] ?? 0;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (n.kind !== "work" || !n.state) continue;
|
||||
counts[n.state] += 1;
|
||||
if (n.state === "queued" || n.state === "running" || n.state === "waiting_approval") {
|
||||
columns[n.state].push(n);
|
||||
} else {
|
||||
columns.done.push(n);
|
||||
}
|
||||
}
|
||||
columns.done.sort((a, b) => Date.parse(b.updatedAt ?? "0") - Date.parse(a.updatedAt ?? "0"));
|
||||
counts.running += comboActive;
|
||||
return { counts, columns };
|
||||
}
|
||||
@@ -95,7 +95,9 @@ export function RadarCatalogTable({ entries, refreshCatalog, onError }: RadarCat
|
||||
}, [onError, t]);
|
||||
|
||||
useEffect(() => {
|
||||
void loadState();
|
||||
void (async () => {
|
||||
await loadState();
|
||||
})();
|
||||
}, [loadState]);
|
||||
|
||||
const stateByKey = useMemo(
|
||||
|
||||
@@ -67,9 +67,15 @@ export default function RadarIntelPage() {
|
||||
}, [load, t]);
|
||||
|
||||
useEffect(() => {
|
||||
load()
|
||||
.catch(() => setError(t("loadFailed")))
|
||||
.finally(() => setLoading(false));
|
||||
void (async () => {
|
||||
try {
|
||||
await load();
|
||||
} catch {
|
||||
setError(t("loadFailed"));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
})();
|
||||
}, [load, t]);
|
||||
|
||||
if (flagOff) notFound();
|
||||
|
||||
@@ -201,7 +201,9 @@ export default function RadarPage() {
|
||||
}, [fetchCatalog, fetchReferrals]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchSettings();
|
||||
void (async () => {
|
||||
await fetchSettings();
|
||||
})();
|
||||
}, [fetchSettings]);
|
||||
|
||||
// Sync (defined before handleActivate which depends on it)
|
||||
@@ -242,7 +244,9 @@ export default function RadarPage() {
|
||||
if (loading || syncing || optIn !== true || autoSyncFiredRef.current) return;
|
||||
if (!shouldAutoSyncOnOpen(meta?.fetchedAt ?? null, Date.now())) return;
|
||||
autoSyncFiredRef.current = true;
|
||||
void handleSync();
|
||||
void (async () => {
|
||||
await handleSync();
|
||||
})();
|
||||
}, [loading, syncing, optIn, meta, handleSync]);
|
||||
|
||||
// Activate opt-in
|
||||
|
||||
@@ -55,7 +55,16 @@ export default function RadarSetupPage() {
|
||||
const provider = searchParams.get("provider");
|
||||
|
||||
const [setupData, setSetupData] = useState<ProviderSetupData | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [loading, setLoading] = useState(provider !== null);
|
||||
|
||||
// Adjust-during-render when the provider query param changes (React docs
|
||||
// pattern): a null provider has nothing to load, any other transition
|
||||
// restarts the loading state before the fetch effect fires.
|
||||
const [prevProvider, setPrevProvider] = useState(provider);
|
||||
if (provider !== prevProvider) {
|
||||
setPrevProvider(provider);
|
||||
setLoading(provider !== null);
|
||||
}
|
||||
const [error, setError] = useState("");
|
||||
const [testing, setTesting] = useState(false);
|
||||
const [testResult, setTestResult] = useState<{ ok: boolean; message: string } | null>(null);
|
||||
@@ -63,7 +72,6 @@ export default function RadarSetupPage() {
|
||||
// Fetch catalog to find the provider's setup data
|
||||
useEffect(() => {
|
||||
if (!provider) {
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -123,12 +131,13 @@ export default function RadarSetupPage() {
|
||||
}, [provider, t]);
|
||||
|
||||
// Test connection — uses the EXISTING connection-test endpoint
|
||||
const connectionId = setupData?.connectionId ?? null;
|
||||
const handleTestConnection = useCallback(async () => {
|
||||
if (!setupData?.connectionId) return;
|
||||
if (!connectionId) return;
|
||||
setTesting(true);
|
||||
setTestResult(null);
|
||||
try {
|
||||
const res = await fetch(`/api/providers/${encodeURIComponent(setupData.connectionId)}/test`, {
|
||||
const res = await fetch(`/api/providers/${encodeURIComponent(connectionId)}/test`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({}),
|
||||
@@ -147,7 +156,7 @@ export default function RadarSetupPage() {
|
||||
} finally {
|
||||
setTesting(false);
|
||||
}
|
||||
}, [setupData?.connectionId, t]);
|
||||
}, [connectionId, t]);
|
||||
|
||||
if (!provider) {
|
||||
return (
|
||||
|
||||
@@ -490,18 +490,18 @@ export default function EvalsTab() {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (targetOptions.length === 0) return;
|
||||
if (targetOptions.some((option) => option.key === selectedTargetKey)) return;
|
||||
// Adjust-during-render (React docs pattern): keep the selected target inside
|
||||
// the current option set, and never compare a target against itself. Both
|
||||
// guards self-extinguish after their setState, so the re-render settles.
|
||||
if (
|
||||
targetOptions.length > 0 &&
|
||||
!targetOptions.some((option) => option.key === selectedTargetKey)
|
||||
) {
|
||||
setSelectedTargetKey(targetOptions[0]?.key || "suite-default:__default__");
|
||||
}, [selectedTargetKey, targetOptions]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!compareTargetKey) return;
|
||||
if (compareTargetKey === selectedTargetKey) {
|
||||
setCompareTargetKey("");
|
||||
}
|
||||
}, [compareTargetKey, selectedTargetKey]);
|
||||
}
|
||||
if (compareTargetKey && compareTargetKey === selectedTargetKey) {
|
||||
setCompareTargetKey("");
|
||||
}
|
||||
|
||||
const filteredSuites = !search.trim()
|
||||
? suites
|
||||
@@ -1846,7 +1846,11 @@ export default function EvalsTab() {
|
||||
);
|
||||
}
|
||||
|
||||
const HeroSection = memo(function HeroSection({ t }: { t: (key: string, values?: Record<string, unknown>) => string }) {
|
||||
const HeroSection = memo(function HeroSection({
|
||||
t,
|
||||
}: {
|
||||
t: (key: string, values?: Record<string, unknown>) => string;
|
||||
}) {
|
||||
return (
|
||||
<Card className="p-0 overflow-hidden">
|
||||
<div
|
||||
|
||||
@@ -41,6 +41,23 @@ interface ResetCreditRequestState {
|
||||
tr: TranslateUsage;
|
||||
}
|
||||
|
||||
// Module-level so the ref-store mutation stays outside any hook body — the
|
||||
// immutability rule bars in-callback writes to `state.idempotencyKeysRef.current`.
|
||||
function resetIdempotencyKeys(keys: React.MutableRefObject<Record<string, string>>): void {
|
||||
keys.current = {};
|
||||
}
|
||||
|
||||
function ensureIdempotencyKey(
|
||||
keys: React.MutableRefObject<Record<string, string>>,
|
||||
selectionToken: string
|
||||
): string {
|
||||
const existing = keys.current[selectionToken];
|
||||
if (existing) return existing;
|
||||
const created = createIdempotencyKey();
|
||||
keys.current[selectionToken] = created;
|
||||
return created;
|
||||
}
|
||||
|
||||
function createIdempotencyKey(): string {
|
||||
return typeof crypto !== "undefined" && typeof crypto.randomUUID === "function"
|
||||
? crypto.randomUUID()
|
||||
@@ -112,9 +129,7 @@ function useRedeemCodexResetCredit(state: ResetCreditRequestState) {
|
||||
async (selectionToken: string) => {
|
||||
const picker = state.resetCreditPicker;
|
||||
if (!picker || state.redeemingResetCreditId || !selectionToken) return;
|
||||
const idempotencyKey =
|
||||
state.idempotencyKeysRef.current[selectionToken] ??
|
||||
(state.idempotencyKeysRef.current[selectionToken] = createIdempotencyKey());
|
||||
const idempotencyKey = ensureIdempotencyKey(state.idempotencyKeysRef, selectionToken);
|
||||
state.setRedeemingResetCreditId(picker.connectionId);
|
||||
state.setErrors((prev) => ({ ...prev, [picker.connectionId]: null }));
|
||||
try {
|
||||
@@ -145,7 +160,7 @@ function useRedeemCodexResetCredit(state: ResetCreditRequestState) {
|
||||
[picker.connectionId]: new Date().toISOString(),
|
||||
}));
|
||||
state.setResetCreditPicker(null);
|
||||
state.idempotencyKeysRef.current = {};
|
||||
resetIdempotencyKeys(state.idempotencyKeysRef);
|
||||
notify.success(state.tr("resetCreditRedeemed", "Reset redeemed"));
|
||||
} catch (error) {
|
||||
const message = getRequestErrorMessage(
|
||||
|
||||
@@ -22,7 +22,9 @@ export default function RateLimitStatus() {
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
load();
|
||||
void (async () => {
|
||||
await load();
|
||||
})();
|
||||
const interval = setInterval(load, 10000);
|
||||
return () => clearInterval(interval);
|
||||
}, [load]);
|
||||
|
||||
@@ -49,7 +49,9 @@ export default function SessionsTab() {
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
loadSessions();
|
||||
void (async () => {
|
||||
await loadSessions();
|
||||
})();
|
||||
const interval = setInterval(loadSessions, 5000);
|
||||
return () => clearInterval(interval);
|
||||
}, [loadSessions]);
|
||||
|
||||
@@ -161,7 +161,9 @@ export default function ProviderQuotaWidget({
|
||||
const [refreshingAll, setRefreshingAll] = useState(false);
|
||||
const [updatedAt, setUpdatedAt] = useState<number | null>(null);
|
||||
const refreshingAllRef = useRef(false);
|
||||
const lastRefreshAllAtRef = useRef(Date.now());
|
||||
// State (not a ref): the countdown renders it, and refs cannot be read during
|
||||
// render nor initialized with Date.now() (purity rule).
|
||||
const [lastRefreshAllAt, setLastRefreshAllAt] = useState(() => Date.now());
|
||||
const autoRefreshIntervalMs = autoRefreshInterval > 0 ? autoRefreshInterval * 1000 : 0;
|
||||
const [autoRefreshClock, setAutoRefreshClock] = useState(() => Date.now());
|
||||
|
||||
@@ -188,14 +190,16 @@ export default function ProviderQuotaWidget({
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
void loadData();
|
||||
void (async () => {
|
||||
await loadData();
|
||||
})();
|
||||
}, [loadData]);
|
||||
|
||||
const refreshAll = useCallback(async () => {
|
||||
if (refreshingAllRef.current) return;
|
||||
refreshingAllRef.current = true;
|
||||
const now = Date.now();
|
||||
lastRefreshAllAtRef.current = now;
|
||||
setLastRefreshAllAt(now);
|
||||
setAutoRefreshClock(now);
|
||||
setRefreshingAll(true);
|
||||
try {
|
||||
@@ -235,10 +239,12 @@ export default function ProviderQuotaWidget({
|
||||
if (document.visibilityState !== "visible") return;
|
||||
if (refreshingAllRef.current) return;
|
||||
|
||||
if (autoRefreshClock - lastRefreshAllAtRef.current >= autoRefreshIntervalMs) {
|
||||
void refreshAll();
|
||||
if (autoRefreshClock - lastRefreshAllAt >= autoRefreshIntervalMs) {
|
||||
void (async () => {
|
||||
await refreshAll();
|
||||
})();
|
||||
}
|
||||
}, [autoRefreshClock, autoRefreshIntervalMs, refreshAll]);
|
||||
}, [autoRefreshClock, lastRefreshAllAt, autoRefreshIntervalMs, refreshAll]);
|
||||
|
||||
const providerGroups = useMemo(() => {
|
||||
const groups = new Map<string, Connection[]>();
|
||||
@@ -286,10 +292,7 @@ export default function ProviderQuotaWidget({
|
||||
? tr("refreshing", "Refreshing")
|
||||
: autoRefreshIntervalMs > 0
|
||||
? `${tr("autoRefreshing", "Auto-refreshing")} ${formatAutoRefreshCountdown(
|
||||
Math.max(
|
||||
0,
|
||||
autoRefreshIntervalMs - (autoRefreshClock - lastRefreshAllAtRef.current)
|
||||
)
|
||||
Math.max(0, autoRefreshIntervalMs - (autoRefreshClock - lastRefreshAllAt))
|
||||
)}`
|
||||
: tr("forceRefresh", "Refresh now")}
|
||||
</button>
|
||||
|
||||
@@ -1432,13 +1432,13 @@ async function buildUnifiedModelsResponseCore(
|
||||
return activeAliases.has(alias) || activeAliases.has(provider);
|
||||
};
|
||||
|
||||
const hasEquivalentSpecialtyModel = (
|
||||
const findEquivalentSpecialtyModel = (
|
||||
providerId: string,
|
||||
rawModelId: string,
|
||||
type: string,
|
||||
scopedModelId: string
|
||||
) =>
|
||||
models.some((model: any) => {
|
||||
models.find((model: any) => {
|
||||
if (model?.id === scopedModelId) return true;
|
||||
if (model?.owned_by !== providerId || model?.type !== type) return false;
|
||||
const existingRoot =
|
||||
@@ -1450,6 +1450,13 @@ async function buildUnifiedModelsResponseCore(
|
||||
return existingRoot === rawModelId;
|
||||
});
|
||||
|
||||
const hasEquivalentSpecialtyModel = (
|
||||
providerId: string,
|
||||
rawModelId: string,
|
||||
type: string,
|
||||
scopedModelId: string
|
||||
) => findEquivalentSpecialtyModel(providerId, rawModelId, type, scopedModelId) !== undefined;
|
||||
|
||||
// Helper: strip the provider prefix from a specialty model ID to get the
|
||||
// provider-relative path (e.g. "openrouter/google/chirp-3" -> "google/chirp-3").
|
||||
// This is the correct key used by the hidden-model lookup — using .split("/").pop()
|
||||
@@ -1464,7 +1471,22 @@ async function buildUnifiedModelsResponseCore(
|
||||
const rawModelId = getSpecialtyModelRelativeId(embModel.id, embModel.provider);
|
||||
if (!providerSupportsModel(embModel.provider, rawModelId)) continue;
|
||||
if (isModelHiddenBulk(embModel.provider, rawModelId)) continue;
|
||||
if (hasEquivalentSpecialtyModel(embModel.provider, rawModelId, "embedding", embModel.id)) {
|
||||
const existingEmbedding = findEquivalentSpecialtyModel(
|
||||
embModel.provider,
|
||||
rawModelId,
|
||||
"embedding",
|
||||
embModel.id
|
||||
);
|
||||
if (existingEmbedding) {
|
||||
// Discovery publishes no vector width, so the registry is the authority.
|
||||
if (embModel.dimensions !== undefined) {
|
||||
existingEmbedding.dimensions = embModel.dimensions;
|
||||
}
|
||||
// A provider that does not report its endpoints leaves the model unclassified. Being in
|
||||
// the embedding registry is that statement, so make it rather than leave it untyped.
|
||||
if (!existingEmbedding.type) {
|
||||
existingEmbedding.type = "embedding";
|
||||
}
|
||||
continue;
|
||||
}
|
||||
models.push({
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -5,7 +5,7 @@ import { NextIntlClientProvider } from "next-intl";
|
||||
import { getMessages, getLocale, getTranslations } from "next-intl/server";
|
||||
import { RTL_LOCALES } from "@/i18n/config";
|
||||
import { normalizeComplianceEventTypes } from "@/i18n/request";
|
||||
import { getSettings } from "@/lib/db/settings";
|
||||
import { getRootLayoutSettings } from "@/lib/db/rootLayoutSettings";
|
||||
import type { Viewport } from "next";
|
||||
import { PwaRegister } from "@/shared/components/PwaRegister";
|
||||
import { LocaleAutoDetect } from "@/shared/components/LocaleAutoDetect";
|
||||
@@ -22,9 +22,9 @@ export const viewport: Viewport = {
|
||||
};
|
||||
|
||||
export async function generateMetadata() {
|
||||
const settings = await getSettings();
|
||||
const instanceName = settings?.instanceName || "OmniRoute";
|
||||
const customFaviconUrl = settings?.customFaviconUrl || settings?.customFaviconBase64;
|
||||
const settings = await getRootLayoutSettings();
|
||||
const instanceName = settings.instanceName;
|
||||
const customFaviconUrl = settings.customFaviconUrl || settings.customFaviconBase64;
|
||||
|
||||
return {
|
||||
title: `${instanceName} — AI Gateway for Multi-Provider LLMs`,
|
||||
|
||||
@@ -319,7 +319,7 @@ export async function registerNodejs(): Promise<void> {
|
||||
process.title = renameProcessTitle(process.title);
|
||||
|
||||
// Initialize proxy fetch patch FIRST (before any HTTP requests)
|
||||
await import("@omniroute/open-sse/index.ts");
|
||||
await import("@omniroute/open-sse/utils/proxyFetch.ts");
|
||||
console.log("[STARTUP] Global fetch proxy patch initialized");
|
||||
|
||||
// Register quota fetchers early so combo routing can use real quota-aware
|
||||
|
||||
@@ -1,29 +1,6 @@
|
||||
import type { SupportedBatchEndpoint } from "@/shared/constants/batchEndpoints";
|
||||
|
||||
type BatchRouteHandler = (request: Request) => Promise<Response> | Response;
|
||||
|
||||
const handlerLoaders: Record<SupportedBatchEndpoint, () => Promise<BatchRouteHandler>> = {
|
||||
"/v1/responses": async () => (await import("@/app/api/v1/responses/route")).POST,
|
||||
"/v1/chat/completions": async () => (await import("@/app/api/v1/chat/completions/route")).POST,
|
||||
"/v1/embeddings": async () => (await import("@/app/api/v1/embeddings/route")).POST,
|
||||
"/v1/completions": async () => (await import("@/app/api/v1/completions/route")).POST,
|
||||
"/v1/moderations": async () => (await import("@/app/api/v1/moderations/route")).POST,
|
||||
"/v1/images/generations": async () =>
|
||||
(await import("@/app/api/v1/images/generations/route")).POST,
|
||||
"/v1/videos/generations": async () =>
|
||||
(await import("@/app/api/v1/videos/generations/route")).POST,
|
||||
};
|
||||
|
||||
const handlerCache = new Map<SupportedBatchEndpoint, BatchRouteHandler>();
|
||||
|
||||
async function getHandler(endpoint: SupportedBatchEndpoint): Promise<BatchRouteHandler> {
|
||||
const cached = handlerCache.get(endpoint);
|
||||
if (cached) return cached;
|
||||
|
||||
const handler = await handlerLoaders[endpoint]();
|
||||
handlerCache.set(endpoint, handler);
|
||||
return handler;
|
||||
}
|
||||
import { getRuntimePorts } from "@/lib/runtime/ports";
|
||||
import { normalizeBasePath } from "@/shared/utils/basePath";
|
||||
|
||||
async function dispatchBatchApiRequest({
|
||||
endpoint,
|
||||
@@ -39,13 +16,17 @@ async function dispatchBatchApiRequest({
|
||||
headers.set("Authorization", `Bearer ${apiKey}`);
|
||||
}
|
||||
|
||||
const handler = await getHandler(endpoint);
|
||||
const request = new Request(`http://localhost${endpoint}`, {
|
||||
const { dashboardPort } = getRuntimePorts();
|
||||
const basePath = normalizeBasePath(process.env.OMNIROUTE_BASE_PATH);
|
||||
const url = `http://127.0.0.1:${dashboardPort}${basePath}${endpoint}`;
|
||||
|
||||
return await globalThis.fetch(url, {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: JSON.stringify(body),
|
||||
// Never follow a redirect while carrying the stored batch API key.
|
||||
redirect: "error",
|
||||
});
|
||||
return await handler(request);
|
||||
}
|
||||
|
||||
export const dispatch = {
|
||||
|
||||
@@ -67,6 +67,7 @@ export const MODE_PACK_OPTIONS = [
|
||||
|
||||
export const ROUTER_STRATEGY_OPTIONS = [
|
||||
{ id: "rules", label: "Rules (6-Factor Scoring)" },
|
||||
{ id: "score", label: "Highest Weighted Score" },
|
||||
{ id: "cost", label: "Cost Optimized" },
|
||||
{ id: "latency", label: "Latency Optimized" },
|
||||
{ id: "sla-aware", label: "SLA-aware" },
|
||||
|
||||
@@ -40,6 +40,7 @@ import { invalidateDbCache } from "./readCache";
|
||||
import { rowToCamel } from "./caseMapping";
|
||||
import { isAutomatedTestProcess } from "@/shared/utils/testProcess";
|
||||
import { parseModelAccessMode } from "./apiKeys/modelAccessMode";
|
||||
import { getExistingDbInstance as getDb, setDbInstance as setDb } from "./singleton";
|
||||
// Re-exported so existing call sites that pull these helpers off the core module keep working.
|
||||
export { toSnakeCase, toCamelCase, objToSnake, rowToCamel, cleanNulls } from "./caseMapping";
|
||||
import {
|
||||
@@ -514,7 +515,6 @@ const SCHEMA_SQL = `
|
||||
// Module-level `let` resets on every webpack recompile, causing connection leaks.
|
||||
|
||||
declare global {
|
||||
var __omnirouteDb: SqliteAdapter | undefined;
|
||||
// Cycle-breaker counter for the probe-failed/restore cascade. Survives
|
||||
// Next.js HMR re-evaluations so concurrent subsystems all see the same
|
||||
// count and we abort with a clear error instead of looping forever.
|
||||
@@ -529,18 +529,6 @@ declare global {
|
||||
var __omnirouteDbOomFailureCount: number | undefined;
|
||||
}
|
||||
|
||||
function getDb(): SqliteDatabase | null {
|
||||
return globalThis.__omnirouteDb ?? null;
|
||||
}
|
||||
|
||||
function setDb(db: SqliteDatabase | null): void {
|
||||
if (db) {
|
||||
globalThis.__omnirouteDb = db;
|
||||
} else {
|
||||
delete globalThis.__omnirouteDb;
|
||||
}
|
||||
}
|
||||
|
||||
function checkpointDb(db: SqliteDatabase, mode: CheckpointMode = "TRUNCATE"): boolean {
|
||||
if (isCloud || isBuildPhase || !SQLITE_FILE) return false;
|
||||
db.pragma(`wal_checkpoint(${mode})`);
|
||||
|
||||
62
src/lib/db/rootLayoutSettings.ts
Normal file
62
src/lib/db/rootLayoutSettings.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { getExistingDbInstance } from "./singleton";
|
||||
|
||||
export interface RootLayoutSettings {
|
||||
instanceName: string;
|
||||
customFaviconUrl: string;
|
||||
customFaviconBase64: string;
|
||||
}
|
||||
|
||||
type RootLayoutSettingKey = keyof RootLayoutSettings;
|
||||
type SettingsRow = {
|
||||
key?: unknown;
|
||||
value?: unknown;
|
||||
};
|
||||
|
||||
const ROOT_LAYOUT_SETTING_KEYS = [
|
||||
"instanceName",
|
||||
"customFaviconUrl",
|
||||
"customFaviconBase64",
|
||||
] as const satisfies readonly RootLayoutSettingKey[];
|
||||
|
||||
const ROOT_LAYOUT_SETTING_KEY_SET = new Set<string>(ROOT_LAYOUT_SETTING_KEYS);
|
||||
|
||||
const DEFAULT_ROOT_LAYOUT_SETTINGS: RootLayoutSettings = {
|
||||
instanceName: "OmniRoute",
|
||||
customFaviconUrl: "",
|
||||
customFaviconBase64: "",
|
||||
};
|
||||
|
||||
function parseStoredString(value: unknown): string | null {
|
||||
if (typeof value !== "string") return null;
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(value) as unknown;
|
||||
return typeof parsed === "string" ? parsed : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** Read only the settings needed while compiling and rendering the root layout. */
|
||||
export async function getRootLayoutSettings(): Promise<RootLayoutSettings> {
|
||||
const db = getExistingDbInstance();
|
||||
if (!db) return { ...DEFAULT_ROOT_LAYOUT_SETTINGS };
|
||||
|
||||
const rows = db
|
||||
.prepare(
|
||||
`SELECT key, value FROM key_value
|
||||
WHERE namespace = 'settings' AND key IN (?, ?, ?)`
|
||||
)
|
||||
.all(...ROOT_LAYOUT_SETTING_KEYS) as SettingsRow[];
|
||||
const settings = { ...DEFAULT_ROOT_LAYOUT_SETTINGS };
|
||||
|
||||
for (const row of rows) {
|
||||
if (typeof row.key !== "string" || !ROOT_LAYOUT_SETTING_KEY_SET.has(row.key)) continue;
|
||||
const value = parseStoredString(row.value);
|
||||
if (value === null) continue;
|
||||
settings[row.key as RootLayoutSettingKey] = value;
|
||||
}
|
||||
|
||||
if (!settings.instanceName) settings.instanceName = DEFAULT_ROOT_LAYOUT_SETTINGS.instanceName;
|
||||
return settings;
|
||||
}
|
||||
19
src/lib/db/singleton.ts
Normal file
19
src/lib/db/singleton.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import type { SqliteAdapter } from "./adapters/types";
|
||||
|
||||
declare global {
|
||||
var __omnirouteDb: SqliteAdapter | undefined;
|
||||
}
|
||||
|
||||
/** Read the process-wide DB handle without initializing storage. */
|
||||
export function getExistingDbInstance(): SqliteAdapter | null {
|
||||
return globalThis.__omnirouteDb ?? null;
|
||||
}
|
||||
|
||||
/** Replace the process-wide DB handle while preserving it across Next.js HMR. */
|
||||
export function setDbInstance(db: SqliteAdapter | null): void {
|
||||
if (db) {
|
||||
globalThis.__omnirouteDb = db;
|
||||
} else {
|
||||
delete globalThis.__omnirouteDb;
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,15 @@ const SHUTDOWN_TIMEOUT_MS = parseInt(process.env.SHUTDOWN_TIMEOUT_MS || "30000",
|
||||
|
||||
declare global {
|
||||
var __omnirouteShutdown:
|
||||
{ init: boolean; shuttingDown: boolean; activeRequests: number } | undefined;
|
||||
| {
|
||||
init: boolean;
|
||||
shuttingDown: boolean;
|
||||
activeRequests: number;
|
||||
shutdownPromise?: Promise<void>;
|
||||
}
|
||||
| undefined;
|
||||
var __omnirouteRequestShutdown: ((signal: string) => Promise<void>) | undefined;
|
||||
var __omnirouteCustomServerOwnsShutdown: boolean | undefined;
|
||||
}
|
||||
|
||||
function getShutdownState() {
|
||||
@@ -102,12 +110,14 @@ async function cleanup(): Promise<void> {
|
||||
{ closeDbInstance },
|
||||
{ flushSpendBatchWriter },
|
||||
{ closeLogRotation },
|
||||
{ closeSharedLoggerResource },
|
||||
{ closeCallLogSaves },
|
||||
] = await Promise.all([
|
||||
import("@omniroute/open-sse/mcp-server/audit.ts"),
|
||||
import("@/lib/db/core"),
|
||||
import("@/lib/spend/batchWriter"),
|
||||
import("@/lib/logRotation"),
|
||||
import("@/shared/utils/loggerResource"),
|
||||
import("@/lib/usage/callLogs"),
|
||||
]);
|
||||
const flushResult = await flushSpendBatchWriter();
|
||||
@@ -123,9 +133,6 @@ async function cleanup(): Promise<void> {
|
||||
if (closeDbInstance()) {
|
||||
console.log("[Shutdown] SQLite database checkpointed and closed.");
|
||||
}
|
||||
closeLogRotation();
|
||||
console.log("[Shutdown] Log rotation timer stopped.");
|
||||
|
||||
// Tear down any persistent VNC login browser containers so they don't leak
|
||||
// past the server process. Best-effort; no-op if the feature was never used
|
||||
// or the docker CLI is unavailable.
|
||||
@@ -147,41 +154,62 @@ async function cleanup(): Promise<void> {
|
||||
} catch {
|
||||
/* feature unused */
|
||||
}
|
||||
|
||||
await closeSharedLoggerResource();
|
||||
closeLogRotation();
|
||||
console.log("[Shutdown] Logger transport and log rotation stopped.");
|
||||
} catch (err) {
|
||||
console.error("[Shutdown] Error during cleanup:", (err as Error).message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the process-wide shutdown sequence, or join the sequence already in progress.
|
||||
*/
|
||||
export function requestGracefulShutdown(signal: string): Promise<void> {
|
||||
const state = getShutdownState();
|
||||
if (state.shutdownPromise) return state.shutdownPromise;
|
||||
|
||||
state.shuttingDown = true;
|
||||
markServerStopping();
|
||||
state.shutdownPromise = (async () => {
|
||||
console.log(`\n[Shutdown] Received ${signal}. Draining ${state.activeRequests} request(s)...`);
|
||||
|
||||
await waitForDrain();
|
||||
await cleanup();
|
||||
|
||||
console.log("[Shutdown] Bye.");
|
||||
})();
|
||||
|
||||
return state.shutdownPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize graceful shutdown handlers.
|
||||
* Should be called once during server startup.
|
||||
*/
|
||||
export function initGracefulShutdown(): void {
|
||||
const state = getShutdownState();
|
||||
globalThis.__omnirouteRequestShutdown ??= requestGracefulShutdown;
|
||||
if (state.init) return;
|
||||
state.init = true;
|
||||
|
||||
const shutdown = async (signal: string) => {
|
||||
if (state.shuttingDown) return;
|
||||
state.shuttingDown = true;
|
||||
markServerStopping();
|
||||
if (globalThis.__omnirouteCustomServerOwnsShutdown) {
|
||||
console.log("[Shutdown] Cleanup registered with the custom server shutdown owner.");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`\n[Shutdown] Received ${signal}. Draining ${state.activeRequests} request(s)...`);
|
||||
|
||||
await waitForDrain();
|
||||
await cleanup();
|
||||
|
||||
console.log("[Shutdown] Bye.");
|
||||
process.exit(0);
|
||||
const shutdown = (signal: string) => {
|
||||
void globalThis.__omnirouteRequestShutdown?.(signal).then(() => process.exit(0));
|
||||
};
|
||||
|
||||
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
||||
process.on("SIGINT", () => shutdown("SIGINT"));
|
||||
process.on("SIGTERM", () => void shutdown("SIGTERM"));
|
||||
process.on("SIGINT", () => void shutdown("SIGINT"));
|
||||
// #8045: on Windows, closing the console window delivers CTRL_CLOSE_EVENT, which
|
||||
// Node/libuv maps to a JS-visible "SIGHUP" event — without this listener, closing
|
||||
// the window never runs cleanup() (WAL checkpoint + closeDbInstance()), leaving
|
||||
// storage.sqlite's WAL un-checkpointed for the next launch.
|
||||
process.on("SIGHUP", () => shutdown("SIGHUP"));
|
||||
process.on("SIGHUP", () => void shutdown("SIGHUP"));
|
||||
|
||||
console.log("[Shutdown] Graceful shutdown handlers registered.");
|
||||
}
|
||||
|
||||
@@ -42,8 +42,18 @@ export function getAppLogRotationCheckInterval(): number {
|
||||
);
|
||||
}
|
||||
|
||||
/** Module-level timer handle — cleared by closeLogRotation(). */
|
||||
let rotationTimer: ReturnType<typeof setInterval> | null = null;
|
||||
interface LogRotationState {
|
||||
timer: ReturnType<typeof setInterval> | null;
|
||||
}
|
||||
|
||||
declare global {
|
||||
var __omnirouteLogRotationState: LogRotationState | undefined;
|
||||
}
|
||||
|
||||
/** Process-wide state survives Next.js development HMR and split server chunks. */
|
||||
function getLogRotationState(): LogRotationState {
|
||||
return (globalThis.__omnirouteLogRotationState ??= { timer: null });
|
||||
}
|
||||
|
||||
export function getLogConfig() {
|
||||
const logToFile = getAppLogToFile();
|
||||
@@ -172,6 +182,9 @@ export function cleanupOverflowLogs(logFilePath: string, maxFiles: number): void
|
||||
* Call closeLogRotation() during application shutdown to clear the timer.
|
||||
*/
|
||||
export function initLogRotation(): void {
|
||||
const state = getLogRotationState();
|
||||
if (state.timer !== null) return;
|
||||
|
||||
const config = getLogConfig();
|
||||
if (!config.logToFile) return;
|
||||
|
||||
@@ -181,7 +194,7 @@ export function initLogRotation(): void {
|
||||
cleanupOverflowLogs(config.logFilePath, config.maxFiles);
|
||||
|
||||
const intervalMs = getAppLogRotationCheckInterval();
|
||||
rotationTimer = setInterval(
|
||||
state.timer = setInterval(
|
||||
(filePath: string, maxSize: number, maxFiles: number) => {
|
||||
rotateIfNeeded(filePath, maxSize);
|
||||
cleanupOverflowLogs(filePath, maxFiles);
|
||||
@@ -191,7 +204,7 @@ export function initLogRotation(): void {
|
||||
config.maxFileSize,
|
||||
config.maxFiles
|
||||
);
|
||||
rotationTimer.unref?.();
|
||||
state.timer.unref?.();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -199,8 +212,9 @@ export function initLogRotation(): void {
|
||||
* Idempotent — safe to call multiple times.
|
||||
*/
|
||||
export function closeLogRotation(): void {
|
||||
if (rotationTimer !== null) {
|
||||
clearInterval(rotationTimer);
|
||||
rotationTimer = null;
|
||||
const state = getLogRotationState();
|
||||
if (state.timer !== null) {
|
||||
clearInterval(state.timer);
|
||||
state.timer = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
joinClaudeCodeCompatibleUrl,
|
||||
joinBaseUrlAndPath,
|
||||
} from "@omniroute/open-sse/services/claudeCodeCompatible.ts";
|
||||
import { getDefaultExecutor } from "@omniroute/open-sse/executors/defaultResolver.ts";
|
||||
import {
|
||||
addModelsSuffix,
|
||||
normalizeAnthropicBaseUrl,
|
||||
@@ -137,8 +138,7 @@ export async function validateClaudeOAuthInline({
|
||||
typeof override === "string" && override ? override : modelId || "claude-haiku-4-5-20251001";
|
||||
|
||||
try {
|
||||
const { getExecutor } = await import("@omniroute/open-sse/executors/index.ts");
|
||||
const executed = await (await getExecutor("claude")).execute({
|
||||
const executed = await getDefaultExecutor("claude").execute({
|
||||
model: testModelId,
|
||||
body: {
|
||||
model: testModelId,
|
||||
|
||||
@@ -22,13 +22,12 @@
|
||||
|
||||
import { logger } from "@omniroute/open-sse/utils/logger.ts";
|
||||
import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/error.ts";
|
||||
import { getExecutor } from "@omniroute/open-sse/executors/index.ts";
|
||||
import type { BaseExecutor } from "@omniroute/open-sse/executors/base";
|
||||
import { getCodexUsage } from "@omniroute/open-sse/services/usage/codex.ts";
|
||||
import { getSettings } from "@/lib/db/settings";
|
||||
import { getProviderConnections, updateProviderConnection } from "@/lib/db/providers";
|
||||
import { isConnectionUnavailableToAuxiliaryActivity } from "@/lib/exclusiveLeaseIsolation";
|
||||
import { refreshAndUpdateCredentials } from "@/lib/usage/providerLimits";
|
||||
import { refreshAndUpdateCredentialsWithResolver } from "@/lib/usage/providerLimits/credentialRefresh";
|
||||
import { getCircuitBreaker } from "@/shared/utils/circuitBreaker";
|
||||
import {
|
||||
QUOTA_AUTOPING_FAILURE_COOLDOWN_MS,
|
||||
@@ -63,8 +62,11 @@ export interface QuotaAutoPingDeps {
|
||||
refreshAndUpdateCredentials: (
|
||||
connection: QuotaAutoPingConnection
|
||||
) => Promise<{ connection: QuotaAutoPingConnection }>;
|
||||
getCodexUsage: (accessToken?: string, providerSpecificData?: JsonRecord) => Promise<JsonRecord>;
|
||||
getExecutor: (provider: string) => Promise<BaseExecutor>;
|
||||
getCodexUsage: (
|
||||
accessToken?: string,
|
||||
providerSpecificData?: JsonRecord
|
||||
) => Promise<JsonRecord>;
|
||||
getExecutor: (provider: "codex") => Promise<BaseExecutor>;
|
||||
canExecuteProvider: (provider: string) => boolean;
|
||||
isConnectionUnavailableToAuxiliaryActivity: (connectionId: string) => Promise<boolean>;
|
||||
}
|
||||
@@ -79,15 +81,33 @@ export function createQuotaAutoPingState(): QuotaAutoPingState {
|
||||
return { running: false, resetCache: {}, failureCache: {} };
|
||||
}
|
||||
|
||||
let codexExecutorPromise: Promise<BaseExecutor> | null = null;
|
||||
|
||||
async function loadQuotaAutoPingExecutor(provider: string): Promise<BaseExecutor> {
|
||||
if (provider !== "codex") {
|
||||
throw new Error(`Quota auto-ping does not support provider "${provider}"`);
|
||||
}
|
||||
|
||||
try {
|
||||
codexExecutorPromise ??= import("@omniroute/open-sse/executors/codex.ts").then(
|
||||
({ CodexExecutor }) => new CodexExecutor()
|
||||
);
|
||||
return await codexExecutorPromise;
|
||||
} catch (error) {
|
||||
codexExecutorPromise = null;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export function createDefaultQuotaAutoPingDeps(): QuotaAutoPingDeps {
|
||||
return {
|
||||
getSettings,
|
||||
getProviderConnections,
|
||||
updateProviderConnection,
|
||||
refreshAndUpdateCredentials: async (connection) =>
|
||||
refreshAndUpdateCredentials(connection as never),
|
||||
refreshAndUpdateCredentialsWithResolver(connection, loadQuotaAutoPingExecutor),
|
||||
getCodexUsage,
|
||||
getExecutor,
|
||||
getExecutor: loadQuotaAutoPingExecutor,
|
||||
canExecuteProvider: (provider) => getCircuitBreaker(provider).canExecute(),
|
||||
isConnectionUnavailableToAuxiliaryActivity,
|
||||
};
|
||||
|
||||
@@ -18,13 +18,10 @@ import { clearRecoveredProviderState } from "@/sse/services/auth";
|
||||
import { getMachineId } from "@/shared/utils/machine";
|
||||
import { USAGE_SUPPORTED_PROVIDERS } from "@/shared/constants/providers";
|
||||
import { mergeProviderLimitsCacheEntry, toProviderLimitsCacheEntry } from "./providerLimitsCache";
|
||||
import { getExecutor } from "@omniroute/open-sse/executors/index.ts";
|
||||
import { getCredentialRefreshExecutor } from "@omniroute/open-sse/executors/credential.ts";
|
||||
import { getUsageForProvider } from "@omniroute/open-sse/services/usage.ts";
|
||||
import { cooldownUntilMs } from "@omniroute/open-sse/services/accountFallback.ts";
|
||||
import {
|
||||
rotationGroupFor,
|
||||
serializeRefresh,
|
||||
} from "@omniroute/open-sse/services/refreshSerializer.ts";
|
||||
import { rotationGroupFor } from "@omniroute/open-sse/services/refreshSerializer.ts";
|
||||
import {
|
||||
extractCodeAssistOnboardTierId,
|
||||
extractCodeAssistSubscriptionTier,
|
||||
@@ -42,29 +39,15 @@ import {
|
||||
sanitizeUsageQuotasForProvider,
|
||||
} from "./providerLimits/quotaNormalize";
|
||||
import { syncInChunksWithSpacing } from "./providerLimits/chunkedSpacingSync";
|
||||
import {
|
||||
refreshAndUpdateCredentialsWithResolver,
|
||||
type CredentialRefreshOptions,
|
||||
type ProviderConnectionLike,
|
||||
} from "./providerLimits/credentialRefresh";
|
||||
export { shouldAttemptRotatingRefresh } from "./providerLimits/credentialRefresh";
|
||||
type JsonRecord = Record<string, unknown>;
|
||||
type SyncSource = "manual" | "scheduled";
|
||||
|
||||
interface ProviderConnectionLike {
|
||||
id: string;
|
||||
provider: string;
|
||||
authType?: string;
|
||||
accessToken?: string;
|
||||
refreshToken?: string;
|
||||
expiresAt?: string;
|
||||
tokenExpiresAt?: string;
|
||||
providerSpecificData?: JsonRecord;
|
||||
testStatus?: string;
|
||||
isActive?: boolean;
|
||||
lastError?: string | null;
|
||||
lastErrorAt?: string | null;
|
||||
lastErrorType?: string | null;
|
||||
lastErrorSource?: string | null;
|
||||
errorCode?: string | number | null;
|
||||
rateLimitedUntil?: string | null;
|
||||
backoffLevel?: number;
|
||||
}
|
||||
|
||||
const PROVIDER_LIMITS_APIKEY_PROVIDERS = new Set([
|
||||
"glm",
|
||||
"glm-cn",
|
||||
@@ -218,122 +201,15 @@ async function syncToCloudIfEnabled() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the quota path may refresh this provider's token. Exported for testing.
|
||||
*
|
||||
* Rotating-refresh providers (Codex/OpenAI share one Auth0 client_id, etc.) mint a
|
||||
* single-use refresh_token on every refresh. The BULK quota-sync path runs many
|
||||
* connections concurrently; refreshing sibling accounts in parallel makes Auth0
|
||||
* revoke the whole token family (openai/codex#9648) and kills every account but
|
||||
* the last (#3019). So the bulk path never refreshes rotating providers
|
||||
* (`allowRotatingRefresh` falsy). The on-demand, per-connection path opts in and
|
||||
* is made safe by `serializeRefresh` (one token mint at a time per rotation group,
|
||||
* so even N concurrent per-account requests can never refresh siblings in
|
||||
* parallel). Non-rotating providers are always eligible.
|
||||
*/
|
||||
export function shouldAttemptRotatingRefresh(
|
||||
provider: string,
|
||||
allowRotatingRefresh: boolean | undefined
|
||||
): boolean {
|
||||
if (rotationGroupFor(provider) === null) return true;
|
||||
return allowRotatingRefresh === true;
|
||||
}
|
||||
|
||||
export async function refreshAndUpdateCredentials(
|
||||
connection: ProviderConnectionLike,
|
||||
opts: { allowRotatingRefresh?: boolean; force?: boolean } = {}
|
||||
opts: CredentialRefreshOptions = {}
|
||||
) {
|
||||
if (!shouldAttemptRotatingRefresh(connection.provider, opts.allowRotatingRefresh)) {
|
||||
return { connection, refreshed: false };
|
||||
}
|
||||
const executor = await getExecutor(connection.provider);
|
||||
const credentials = {
|
||||
connectionId: connection.id,
|
||||
accessToken: connection.accessToken,
|
||||
refreshToken: connection.refreshToken,
|
||||
expiresAt: connection.tokenExpiresAt || connection.expiresAt || null,
|
||||
providerSpecificData: connection.providerSpecificData,
|
||||
copilotToken: connection.providerSpecificData?.copilotToken,
|
||||
copilotTokenExpiresAt: connection.providerSpecificData?.copilotTokenExpiresAt,
|
||||
};
|
||||
|
||||
// `force` is used ONLY on the reactive 401 recovery path (a usage fetch came
|
||||
// back unauthorized) — it bypasses the proactive `needsRefresh` heuristic so
|
||||
// imported accounts (expiresAt=null, where needsRefresh is always false) can
|
||||
// still re-mint. The mint stays serialized per rotation group; this never
|
||||
// refreshes proactively from the bulk path (#3019 guard above is unchanged).
|
||||
if (!opts.force && !executor.needsRefresh(credentials)) {
|
||||
return { connection, refreshed: false };
|
||||
}
|
||||
|
||||
// Serialize the actual token mint per rotation group so two sibling accounts
|
||||
// never hit Auth0 concurrently (passthrough for non-rotating providers).
|
||||
const refreshResult = (await serializeRefresh(connection.provider, () =>
|
||||
executor.refreshCredentials(credentials, console)
|
||||
)) as
|
||||
| (JsonRecord & {
|
||||
accessToken?: string;
|
||||
refreshToken?: string;
|
||||
expiresIn?: number;
|
||||
expiresAt?: string;
|
||||
copilotToken?: string;
|
||||
copilotTokenExpiresAt?: string;
|
||||
})
|
||||
| null;
|
||||
|
||||
if (!refreshResult) {
|
||||
// Refresh failed but we still have an accessToken — fall back to the
|
||||
// existing token for ANY OAuth provider (graceful degradation) instead of
|
||||
// hard-failing. Previously this was qualified to `provider === "github"`,
|
||||
// which left every other provider stuck on a transient refresh failure even
|
||||
// when a usable access token was still on hand.
|
||||
if (connection.accessToken) {
|
||||
return { connection, refreshed: false };
|
||||
}
|
||||
throw withStatus(
|
||||
new Error("Failed to refresh credentials. Please re-authorize the connection."),
|
||||
401
|
||||
);
|
||||
}
|
||||
|
||||
const updateData: JsonRecord = {
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
if (refreshResult.accessToken) {
|
||||
updateData.accessToken = refreshResult.accessToken;
|
||||
}
|
||||
if (refreshResult.refreshToken) {
|
||||
updateData.refreshToken = refreshResult.refreshToken;
|
||||
}
|
||||
if (refreshResult.expiresIn) {
|
||||
const expiresAt = new Date(Date.now() + refreshResult.expiresIn * 1000).toISOString();
|
||||
updateData.expiresAt = expiresAt;
|
||||
updateData.tokenExpiresAt = expiresAt;
|
||||
} else if (refreshResult.expiresAt) {
|
||||
updateData.expiresAt = refreshResult.expiresAt;
|
||||
updateData.tokenExpiresAt = refreshResult.expiresAt;
|
||||
}
|
||||
if (refreshResult.copilotToken || refreshResult.copilotTokenExpiresAt) {
|
||||
updateData.providerSpecificData = {
|
||||
...(connection.providerSpecificData || {}),
|
||||
copilotToken: refreshResult.copilotToken,
|
||||
copilotTokenExpiresAt: refreshResult.copilotTokenExpiresAt,
|
||||
};
|
||||
}
|
||||
|
||||
await updateProviderConnection(connection.id, updateData);
|
||||
|
||||
return {
|
||||
connection: {
|
||||
...connection,
|
||||
...updateData,
|
||||
providerSpecificData:
|
||||
(updateData.providerSpecificData as JsonRecord | undefined) ||
|
||||
connection.providerSpecificData,
|
||||
},
|
||||
refreshed: true,
|
||||
};
|
||||
return refreshAndUpdateCredentialsWithResolver(
|
||||
connection,
|
||||
getCredentialRefreshExecutor,
|
||||
opts
|
||||
);
|
||||
}
|
||||
|
||||
function isUsageAuthError(message: unknown): boolean {
|
||||
|
||||
150
src/lib/usage/providerLimits/credentialRefresh.ts
Normal file
150
src/lib/usage/providerLimits/credentialRefresh.ts
Normal file
@@ -0,0 +1,150 @@
|
||||
import { updateProviderConnection } from "@/lib/db/providers";
|
||||
import type { BaseExecutor } from "@omniroute/open-sse/executors/base";
|
||||
import {
|
||||
rotationGroupFor,
|
||||
serializeRefresh,
|
||||
} from "@omniroute/open-sse/services/refreshSerializer.ts";
|
||||
|
||||
type JsonRecord = Record<string, unknown>;
|
||||
|
||||
type CredentialRefreshResult = JsonRecord & {
|
||||
accessToken?: string;
|
||||
refreshToken?: string;
|
||||
expiresIn?: number;
|
||||
expiresAt?: string;
|
||||
copilotToken?: string;
|
||||
copilotTokenExpiresAt?: string;
|
||||
};
|
||||
|
||||
export interface ProviderConnectionLike {
|
||||
id: string;
|
||||
provider: string;
|
||||
authType?: string;
|
||||
accessToken?: string;
|
||||
refreshToken?: string;
|
||||
expiresAt?: string | null;
|
||||
tokenExpiresAt?: string | null;
|
||||
providerSpecificData?: JsonRecord;
|
||||
testStatus?: string;
|
||||
isActive?: boolean;
|
||||
lastError?: string | null;
|
||||
lastErrorAt?: string | null;
|
||||
lastErrorType?: string | null;
|
||||
lastErrorSource?: string | null;
|
||||
errorCode?: string | number | null;
|
||||
rateLimitedUntil?: string | null;
|
||||
backoffLevel?: number;
|
||||
}
|
||||
|
||||
export interface CredentialRefreshOptions {
|
||||
allowRotatingRefresh?: boolean;
|
||||
force?: boolean;
|
||||
}
|
||||
|
||||
export type CredentialExecutorResolver = (provider: string) => Promise<BaseExecutor>;
|
||||
|
||||
function withStatus(error: Error, status: number): Error & { status: number } {
|
||||
return Object.assign(error, { status });
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the quota path may refresh this provider's token.
|
||||
*
|
||||
* Rotating-refresh providers mint a single-use refresh token on every refresh,
|
||||
* so bulk quota sync must not refresh siblings concurrently. The on-demand path
|
||||
* explicitly opts in and remains serialized per rotation group.
|
||||
*/
|
||||
export function shouldAttemptRotatingRefresh(
|
||||
provider: string,
|
||||
allowRotatingRefresh: boolean | undefined
|
||||
): boolean {
|
||||
if (rotationGroupFor(provider) === null) return true;
|
||||
return allowRotatingRefresh === true;
|
||||
}
|
||||
|
||||
function buildCredentialUpdateData(
|
||||
connection: ProviderConnectionLike,
|
||||
refreshResult: CredentialRefreshResult
|
||||
): JsonRecord {
|
||||
const updateData: JsonRecord = {
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
if (refreshResult.accessToken) {
|
||||
updateData.accessToken = refreshResult.accessToken;
|
||||
}
|
||||
if (refreshResult.refreshToken) {
|
||||
updateData.refreshToken = refreshResult.refreshToken;
|
||||
}
|
||||
if (refreshResult.expiresIn) {
|
||||
const expiresAt = new Date(Date.now() + refreshResult.expiresIn * 1000).toISOString();
|
||||
updateData.expiresAt = expiresAt;
|
||||
updateData.tokenExpiresAt = expiresAt;
|
||||
} else if (refreshResult.expiresAt) {
|
||||
updateData.expiresAt = refreshResult.expiresAt;
|
||||
updateData.tokenExpiresAt = refreshResult.expiresAt;
|
||||
}
|
||||
if (refreshResult.copilotToken || refreshResult.copilotTokenExpiresAt) {
|
||||
updateData.providerSpecificData = {
|
||||
...(connection.providerSpecificData || {}),
|
||||
copilotToken: refreshResult.copilotToken,
|
||||
copilotTokenExpiresAt: refreshResult.copilotTokenExpiresAt,
|
||||
};
|
||||
}
|
||||
|
||||
return updateData;
|
||||
}
|
||||
|
||||
/** Refresh and persist credentials using a caller-supplied executor resolver. */
|
||||
export async function refreshAndUpdateCredentialsWithResolver(
|
||||
connection: ProviderConnectionLike,
|
||||
resolveExecutor: CredentialExecutorResolver,
|
||||
opts: CredentialRefreshOptions = {}
|
||||
) {
|
||||
if (!shouldAttemptRotatingRefresh(connection.provider, opts.allowRotatingRefresh)) {
|
||||
return { connection, refreshed: false };
|
||||
}
|
||||
const executor = await resolveExecutor(connection.provider);
|
||||
const credentials = {
|
||||
connectionId: connection.id,
|
||||
accessToken: connection.accessToken,
|
||||
refreshToken: connection.refreshToken,
|
||||
expiresAt: connection.tokenExpiresAt || connection.expiresAt || null,
|
||||
providerSpecificData: connection.providerSpecificData,
|
||||
copilotToken: connection.providerSpecificData?.copilotToken,
|
||||
copilotTokenExpiresAt: connection.providerSpecificData?.copilotTokenExpiresAt,
|
||||
};
|
||||
|
||||
if (!opts.force && !executor.needsRefresh(credentials)) {
|
||||
return { connection, refreshed: false };
|
||||
}
|
||||
|
||||
const refreshResult = (await serializeRefresh(connection.provider, () =>
|
||||
executor.refreshCredentials(credentials, console)
|
||||
)) as CredentialRefreshResult | null;
|
||||
|
||||
if (!refreshResult) {
|
||||
if (connection.accessToken) {
|
||||
return { connection, refreshed: false };
|
||||
}
|
||||
throw withStatus(
|
||||
new Error("Failed to refresh credentials. Please re-authorize the connection."),
|
||||
401
|
||||
);
|
||||
}
|
||||
|
||||
const updateData = buildCredentialUpdateData(connection, refreshResult);
|
||||
|
||||
await updateProviderConnection(connection.id, updateData);
|
||||
|
||||
return {
|
||||
connection: {
|
||||
...connection,
|
||||
...updateData,
|
||||
providerSpecificData:
|
||||
(updateData.providerSpecificData as JsonRecord | undefined) ||
|
||||
connection.providerSpecificData,
|
||||
},
|
||||
refreshed: true,
|
||||
};
|
||||
}
|
||||
@@ -55,6 +55,16 @@ import {
|
||||
// Reduced from 300 → 50 to avoid browser freeze and network saturation.
|
||||
const PAGE_SIZE = 50;
|
||||
|
||||
// Column sort toggle mapping: clicking a column header toggles asc/desc.
|
||||
const COLUMN_SORT_MAP = {
|
||||
status: { desc: "status_desc", asc: "status_asc" },
|
||||
model: { desc: "model_desc", asc: "model_asc" },
|
||||
tokens: { desc: "tokens_desc", asc: "tokens_asc" },
|
||||
tps: { desc: "tps_desc", asc: "tps_asc" },
|
||||
duration: { desc: "duration_desc", asc: "duration_asc" },
|
||||
time: { desc: "newest", asc: "oldest" },
|
||||
} as const;
|
||||
|
||||
function getLogTotalTokens(log) {
|
||||
return (log?.tokens?.in || 0) + (log?.tokens?.out || 0);
|
||||
}
|
||||
@@ -147,17 +157,8 @@ const RequestLoggerV2 = forwardRef<RequestLoggerV2Handle, { initialSelectedId?:
|
||||
const [groupedView, setGroupedView] = useState(false);
|
||||
const [detailLoading, setDetailLoading] = useState(false);
|
||||
|
||||
// Column sort toggle: clicking a column header toggles asc/desc
|
||||
const columnSortMap = {
|
||||
status: { desc: "status_desc", asc: "status_asc" },
|
||||
model: { desc: "model_desc", asc: "model_asc" },
|
||||
tokens: { desc: "tokens_desc", asc: "tokens_asc" },
|
||||
tps: { desc: "tps_desc", asc: "tps_asc" },
|
||||
duration: { desc: "duration_desc", asc: "duration_asc" },
|
||||
time: { desc: "newest", asc: "oldest" },
|
||||
};
|
||||
const toggleSort = useCallback((column: string) => {
|
||||
const mapping = columnSortMap[column as keyof typeof columnSortMap];
|
||||
const mapping = COLUMN_SORT_MAP[column as keyof typeof COLUMN_SORT_MAP];
|
||||
if (!mapping) return;
|
||||
setSortBy((prev) => {
|
||||
if (prev === mapping.desc) return mapping.asc;
|
||||
@@ -166,7 +167,7 @@ const RequestLoggerV2 = forwardRef<RequestLoggerV2Handle, { initialSelectedId?:
|
||||
}, []);
|
||||
const getSortIndicator = useCallback(
|
||||
(column: string) => {
|
||||
const mapping = columnSortMap[column as keyof typeof columnSortMap];
|
||||
const mapping = COLUMN_SORT_MAP[column as keyof typeof COLUMN_SORT_MAP];
|
||||
if (!mapping) return "";
|
||||
if (sortBy === mapping.desc) return " ↓";
|
||||
if (sortBy === mapping.asc) return " ↑";
|
||||
@@ -535,89 +536,7 @@ const RequestLoggerV2 = forwardRef<RequestLoggerV2Handle, { initialSelectedId?:
|
||||
// endpoint until the row appears.
|
||||
const router = useRouter();
|
||||
|
||||
const openDetail = async (logEntry) => {
|
||||
// Guard: if no valid id provided, close instead of opening an empty modal
|
||||
if (!logEntry?.id) {
|
||||
try {
|
||||
closeDetail();
|
||||
} catch {}
|
||||
return;
|
||||
}
|
||||
|
||||
const requestToken = `${logEntry.id}:${Date.now()}:${Math.random()}`;
|
||||
detailRequestRef.current = requestToken;
|
||||
const isCurrentDetailRequest = () => detailRequestRef.current === requestToken;
|
||||
|
||||
setSelectedLog(logEntry);
|
||||
try {
|
||||
const url = new URL(globalThis.location.href);
|
||||
url.searchParams.set("id", logEntry.id);
|
||||
router.replace(url.pathname + url.search, { scroll: false });
|
||||
} catch (e) {
|
||||
// ignore navigation errors
|
||||
}
|
||||
setDetailLoading(true);
|
||||
setDetailData(null);
|
||||
try {
|
||||
const res = await fetch(`/api/logs/${logEntry.id}`, { cache: "no-store" });
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
if (!isCurrentDetailRequest()) return;
|
||||
const dataHasPipeline =
|
||||
data?.pipelinePayloads && Object.keys(data.pipelinePayloads || {}).length > 0;
|
||||
setDetailData((prev: { pipelinePayloads: any }) => ({
|
||||
...prev,
|
||||
...data,
|
||||
pipelinePayloads: dataHasPipeline ? data.pipelinePayloads : prev?.pipelinePayloads,
|
||||
}));
|
||||
// ensure the modal summary reflects the fetched call log summary
|
||||
if (data && typeof data === "object") {
|
||||
setSelectedLog((prev: any) => ({
|
||||
...prev,
|
||||
...data,
|
||||
active: data.active === true,
|
||||
}));
|
||||
}
|
||||
} else {
|
||||
// A deep-linked id can legitimately 404 while the request is still
|
||||
// finalizing. Keep the modal open and poll /api/logs/[id] instead of
|
||||
// falling back to an in-memory active-request endpoint.
|
||||
if (!isCurrentDetailRequest()) return;
|
||||
if (res.status === 404) {
|
||||
if (logEntry.pendingLookup || logEntry.active) {
|
||||
setSelectedLog((prev: { method: any; path: any }) => ({
|
||||
...prev,
|
||||
id: logEntry.id,
|
||||
status: 0,
|
||||
method: prev?.method,
|
||||
path: prev?.path || "",
|
||||
}));
|
||||
setDetailData({ detailState: "pending" });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
console.warn("Log not found:", logEntry.id);
|
||||
} catch {}
|
||||
try {
|
||||
closeDetail();
|
||||
} catch {}
|
||||
return;
|
||||
}
|
||||
// other errors: show a minimal error indicator by setting detailData to an error object
|
||||
try {
|
||||
const body = await res.text().catch(() => null);
|
||||
if (!isCurrentDetailRequest()) return;
|
||||
setDetailData({ error: `Failed to fetch log (status ${res.status})`, body });
|
||||
} catch {}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch log detail:", error);
|
||||
} finally {
|
||||
if (isCurrentDetailRequest()) setDetailLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const closeDetail = () => {
|
||||
const closeDetail = useCallback(() => {
|
||||
detailRequestRef.current = "";
|
||||
setSelectedLog(null);
|
||||
setDetailData(null);
|
||||
@@ -629,7 +548,92 @@ const RequestLoggerV2 = forwardRef<RequestLoggerV2Handle, { initialSelectedId?:
|
||||
} catch (e) {
|
||||
// ignore navigation errors
|
||||
}
|
||||
};
|
||||
}, [router]);
|
||||
|
||||
const openDetail = useCallback(
|
||||
async (logEntry) => {
|
||||
// Guard: if no valid id provided, close instead of opening an empty modal
|
||||
if (!logEntry?.id) {
|
||||
try {
|
||||
closeDetail();
|
||||
} catch {}
|
||||
return;
|
||||
}
|
||||
|
||||
const requestToken = `${logEntry.id}:${Date.now()}:${Math.random()}`;
|
||||
detailRequestRef.current = requestToken;
|
||||
const isCurrentDetailRequest = () => detailRequestRef.current === requestToken;
|
||||
|
||||
setSelectedLog(logEntry);
|
||||
try {
|
||||
const url = new URL(globalThis.location.href);
|
||||
url.searchParams.set("id", logEntry.id);
|
||||
router.replace(url.pathname + url.search, { scroll: false });
|
||||
} catch (e) {
|
||||
// ignore navigation errors
|
||||
}
|
||||
setDetailLoading(true);
|
||||
setDetailData(null);
|
||||
try {
|
||||
const res = await fetch(`/api/logs/${logEntry.id}`, { cache: "no-store" });
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
if (!isCurrentDetailRequest()) return;
|
||||
const dataHasPipeline =
|
||||
data?.pipelinePayloads && Object.keys(data.pipelinePayloads || {}).length > 0;
|
||||
setDetailData((prev: { pipelinePayloads: any }) => ({
|
||||
...prev,
|
||||
...data,
|
||||
pipelinePayloads: dataHasPipeline ? data.pipelinePayloads : prev?.pipelinePayloads,
|
||||
}));
|
||||
// ensure the modal summary reflects the fetched call log summary
|
||||
if (data && typeof data === "object") {
|
||||
setSelectedLog((prev: any) => ({
|
||||
...prev,
|
||||
...data,
|
||||
active: data.active === true,
|
||||
}));
|
||||
}
|
||||
} else {
|
||||
// A deep-linked id can legitimately 404 while the request is still
|
||||
// finalizing. Keep the modal open and poll /api/logs/[id] instead of
|
||||
// falling back to an in-memory active-request endpoint.
|
||||
if (!isCurrentDetailRequest()) return;
|
||||
if (res.status === 404) {
|
||||
if (logEntry.pendingLookup || logEntry.active) {
|
||||
setSelectedLog((prev: { method: any; path: any }) => ({
|
||||
...prev,
|
||||
id: logEntry.id,
|
||||
status: 0,
|
||||
method: prev?.method,
|
||||
path: prev?.path || "",
|
||||
}));
|
||||
setDetailData({ detailState: "pending" });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
console.warn("Log not found:", logEntry.id);
|
||||
} catch {}
|
||||
try {
|
||||
closeDetail();
|
||||
} catch {}
|
||||
return;
|
||||
}
|
||||
// other errors: show a minimal error indicator by setting detailData to an error object
|
||||
try {
|
||||
const body = await res.text().catch(() => null);
|
||||
if (!isCurrentDetailRequest()) return;
|
||||
setDetailData({ error: `Failed to fetch log (status ${res.status})`, body });
|
||||
} catch {}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch log detail:", error);
|
||||
} finally {
|
||||
if (isCurrentDetailRequest()) setDetailLoading(false);
|
||||
}
|
||||
},
|
||||
[closeDetail, router]
|
||||
);
|
||||
|
||||
const sortedLogsForNav = useMemo(() => sortedLogs, [sortedLogs]);
|
||||
|
||||
@@ -654,7 +658,7 @@ const RequestLoggerV2 = forwardRef<RequestLoggerV2Handle, { initialSelectedId?:
|
||||
console.error("Failed to open initial log id:", error_);
|
||||
});
|
||||
}
|
||||
}, [initialSelectedId]);
|
||||
}, [initialSelectedId, openDetail]);
|
||||
|
||||
useEffect(() => {
|
||||
const isActive = selectedLog?.active === true;
|
||||
@@ -765,7 +769,7 @@ const RequestLoggerV2 = forwardRef<RequestLoggerV2Handle, { initialSelectedId?:
|
||||
pendingBoundaryNavRef.current = "prev";
|
||||
fetchLogs(false);
|
||||
}
|
||||
}, [currentLogIndex, sortedLogsForNav, fetchLogs]);
|
||||
}, [currentLogIndex, sortedLogsForNav, fetchLogs, openDetail]);
|
||||
|
||||
const handleNext = useCallback(() => {
|
||||
const idx = currentLogIndex;
|
||||
@@ -781,7 +785,7 @@ const RequestLoggerV2 = forwardRef<RequestLoggerV2Handle, { initialSelectedId?:
|
||||
pendingBoundaryNavRef.current = "next";
|
||||
fetchLogs(false);
|
||||
}
|
||||
}, [currentLogIndex, sortedLogsForNav, fetchLogs]);
|
||||
}, [currentLogIndex, sortedLogsForNav, fetchLogs, openDetail]);
|
||||
|
||||
// Resolves a pending boundary nav (see handlePrev/handleNext) once a
|
||||
// triggered fetchLogs() resync has landed in sortedLogsForNav. Only fires
|
||||
|
||||
@@ -37,6 +37,7 @@ export type AnyRoutingStrategyValue = RoutingStrategyValue | InternalRoutingStra
|
||||
|
||||
export const AUTO_ROUTING_STRATEGY_VALUES = [
|
||||
"rules",
|
||||
"score",
|
||||
"cost",
|
||||
"eco",
|
||||
"latency",
|
||||
|
||||
@@ -14,4 +14,6 @@ export const STATUS_HEX = {
|
||||
warning: "#f59e0b",
|
||||
error: "#ef4444",
|
||||
muted: "#6b7280",
|
||||
/** Human-approval gate (waiting_approval) — violet, matching the industry de-facto palette. */
|
||||
approval: "#8b5cf6",
|
||||
} as const;
|
||||
|
||||
@@ -42,6 +42,13 @@ export function isClientAbortError(err) {
|
||||
const e = /** @type {NodeJS.ErrnoException} */ (err);
|
||||
// Node emits `Error: aborted` (no code) from http.Server#abortIncoming.
|
||||
if (e.message === "aborted" || e.message === "Aborted") return true;
|
||||
// OmniRoute's SSE teardown aborts in-flight legs with
|
||||
// `Error [AbortError]: request_signal_aborted` on client disconnects
|
||||
// (open-sse/utils/streamHandler.ts), and fetch/DOM cancellation surfaces as
|
||||
// `AbortError` with an abort-flavoured message. Same benign class as
|
||||
// `Error: aborted` — an emitter-left 'error' event on any of these used to
|
||||
// kill the process (#fix-dev-server-aborted).
|
||||
if (e.name === "AbortError" && /abort/i.test(String(e.message))) return true;
|
||||
switch (e.code) {
|
||||
case "ERR_STREAM_PREMATURE_CLOSE":
|
||||
case "ECONNRESET":
|
||||
|
||||
@@ -18,6 +18,10 @@ import { resolve } from "path";
|
||||
import { getLogConfig, initLogRotation } from "@/lib/logRotation";
|
||||
import { getAppLogLevel } from "@/lib/logEnv";
|
||||
import { redactLogArgs } from "@/shared/utils/logRedaction";
|
||||
import {
|
||||
getOrCreateSharedLoggerResource,
|
||||
type SharedLoggerResource,
|
||||
} from "@/shared/utils/loggerResource";
|
||||
|
||||
const isDev = process.env.NODE_ENV !== "production";
|
||||
|
||||
@@ -61,7 +65,7 @@ function getTransportCompatibleConfig(): pino.LoggerOptions {
|
||||
* vanished, so failed writes are dropped (best-effort stderr notice) instead of
|
||||
* escalating.
|
||||
*/
|
||||
function buildFileTransportStream(targets: NonNullable<pino.TransportMultiOptions["targets"]>) {
|
||||
function buildTransportStream(targets: NonNullable<pino.TransportMultiOptions["targets"]>) {
|
||||
const stream = pino.transport({ targets });
|
||||
stream.on("error", (err: unknown) => {
|
||||
try {
|
||||
@@ -75,11 +79,65 @@ function buildFileTransportStream(targets: NonNullable<pino.TransportMultiOption
|
||||
return stream;
|
||||
}
|
||||
|
||||
interface OwnedLogStream {
|
||||
flushSync?: () => void;
|
||||
end?: () => void;
|
||||
once?: (event: string, listener: () => void) => unknown;
|
||||
}
|
||||
|
||||
async function closeOwnedStream(stream: OwnedLogStream | null): Promise<void> {
|
||||
if (!stream) return;
|
||||
|
||||
try {
|
||||
stream.flushSync?.();
|
||||
} catch {
|
||||
// Best-effort shutdown: a missing log destination must not block process exit.
|
||||
}
|
||||
|
||||
if (!stream.end) return;
|
||||
|
||||
await new Promise<void>((resolveClose) => {
|
||||
let resolved = false;
|
||||
const finish = () => {
|
||||
if (resolved) return;
|
||||
resolved = true;
|
||||
resolveClose();
|
||||
};
|
||||
const fallback = setTimeout(finish, 1_000);
|
||||
|
||||
stream.once?.("close", () => {
|
||||
clearTimeout(fallback);
|
||||
finish();
|
||||
});
|
||||
|
||||
try {
|
||||
stream.end?.();
|
||||
if (!stream.once) {
|
||||
clearTimeout(fallback);
|
||||
finish();
|
||||
}
|
||||
} catch {
|
||||
clearTimeout(fallback);
|
||||
finish();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function createLoggerResource(
|
||||
logger: pino.Logger,
|
||||
stream: OwnedLogStream | null
|
||||
): SharedLoggerResource {
|
||||
return {
|
||||
logger,
|
||||
close: () => closeOwnedStream(stream),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the logger with optional file transport.
|
||||
* Uses pino transport targets for all destinations.
|
||||
*/
|
||||
function buildLogger(): pino.Logger {
|
||||
function buildLoggerResource(): SharedLoggerResource {
|
||||
const logConfig = getLogConfig();
|
||||
const logLevel = (baseConfig.level as string) || "info";
|
||||
const transportConfig = getTransportCompatibleConfig();
|
||||
@@ -95,7 +153,7 @@ function buildLogger(): pino.Logger {
|
||||
|
||||
if (isDev) {
|
||||
// Dev: pino-pretty → stdout, JSON → file
|
||||
const stream = buildFileTransportStream([
|
||||
const stream = buildTransportStream([
|
||||
{
|
||||
target: "pino-pretty",
|
||||
options: {
|
||||
@@ -113,12 +171,12 @@ function buildLogger(): pino.Logger {
|
||||
level: logLevel,
|
||||
},
|
||||
]);
|
||||
return pino(transportConfig, stream);
|
||||
return createLoggerResource(pino(transportConfig, stream), stream);
|
||||
}
|
||||
|
||||
// Production: JSON → stdout + JSON → file
|
||||
{
|
||||
const stream = buildFileTransportStream([
|
||||
const stream = buildTransportStream([
|
||||
{
|
||||
target: "pino/file",
|
||||
options: { destination: 1 }, // stdout
|
||||
@@ -130,7 +188,7 @@ function buildLogger(): pino.Logger {
|
||||
level: logLevel,
|
||||
},
|
||||
]);
|
||||
return pino(transportConfig, stream);
|
||||
return createLoggerResource(pino(transportConfig, stream), stream);
|
||||
}
|
||||
} catch (err) {
|
||||
// Log the actual error for diagnostics (issue #165)
|
||||
@@ -156,12 +214,15 @@ function buildLogger(): pino.Logger {
|
||||
});
|
||||
|
||||
// Production fallback: JSON to both stdout and file via multistream
|
||||
return pino(
|
||||
baseConfig,
|
||||
pino.multistream([
|
||||
{ stream: process.stdout, level: logLevel as pino.Level },
|
||||
{ stream: fileDestination, level: logLevel as pino.Level },
|
||||
])
|
||||
return createLoggerResource(
|
||||
pino(
|
||||
baseConfig,
|
||||
pino.multistream([
|
||||
{ stream: process.stdout, level: logLevel as pino.Level },
|
||||
{ stream: fileDestination, level: logLevel as pino.Level },
|
||||
])
|
||||
),
|
||||
fileDestination
|
||||
);
|
||||
} catch (fallbackErr) {
|
||||
try {
|
||||
@@ -175,9 +236,8 @@ function buildLogger(): pino.Logger {
|
||||
|
||||
// Console-only (no file logging)
|
||||
if (isDev) {
|
||||
return pino({
|
||||
...baseConfig,
|
||||
transport: {
|
||||
const stream = buildTransportStream([
|
||||
{
|
||||
target: "pino-pretty",
|
||||
options: {
|
||||
colorize: true,
|
||||
@@ -185,14 +245,18 @@ function buildLogger(): pino.Logger {
|
||||
ignore: "pid,hostname,service",
|
||||
messageFormat: "[{module}] {msg}",
|
||||
},
|
||||
level: logLevel,
|
||||
},
|
||||
});
|
||||
]);
|
||||
return createLoggerResource(pino(transportConfig, stream), stream);
|
||||
}
|
||||
|
||||
return pino(baseConfig);
|
||||
return createLoggerResource(pino(baseConfig), null);
|
||||
}
|
||||
|
||||
export const logger = buildLogger();
|
||||
const sharedLoggerResource = getOrCreateSharedLoggerResource(buildLoggerResource);
|
||||
|
||||
export const logger = sharedLoggerResource.logger;
|
||||
|
||||
/**
|
||||
* Create a child logger with a module tag.
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user