mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-10 17:22:17 +03:00
Compare commits
13 Commits
feat/9490-
...
maint/cher
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
65c31f2286 | ||
|
|
b9dc3a7d5c | ||
|
|
2d5e9fad45 | ||
|
|
81fc56a369 | ||
|
|
a390aa4340 | ||
|
|
f3e48df597 | ||
|
|
a4bdd15c98 | ||
|
|
873cf1ffa3 | ||
|
|
40df6e0a0a | ||
|
|
aae408f585 | ||
|
|
3e1c31c606 | ||
|
|
2e12ee89f7 | ||
|
|
723ce0b166 |
1
.eval_score
Normal file
1
.eval_score
Normal file
@@ -0,0 +1 @@
|
||||
10
|
||||
20
.gitignore
vendored
20
.gitignore
vendored
@@ -72,7 +72,6 @@ yarn-error.log*
|
||||
# env files (can opt-in for committing if needed)
|
||||
.env*
|
||||
!.env.example
|
||||
!.env.devin-bridge.example
|
||||
!.env.homolog.example
|
||||
# Provider API keys (never commit)
|
||||
*.api-key
|
||||
@@ -210,8 +209,6 @@ scripts/i18n/_pending-keys.json
|
||||
.agents/
|
||||
.antigravitycli/
|
||||
.claude/
|
||||
!tests/fixtures/devin-bridge/e2e-workspace/.claude/
|
||||
!tests/fixtures/devin-bridge/e2e-workspace/.claude/**
|
||||
|
||||
# PR Reviews and local feedback files
|
||||
pr_reviews*.json
|
||||
@@ -238,10 +235,7 @@ omniroute.md
|
||||
|
||||
# mise configuration
|
||||
mise.toml
|
||||
# release-green artifacts (.gitignore has no inline comments — a trailing
|
||||
# `# ...` becomes part of the pattern, so it must sit on its own line).
|
||||
# Already covered by /_*/ above; kept explicit for discoverability.
|
||||
_artifacts/
|
||||
_artifacts/ # release-green artifacts
|
||||
.claude-flow/
|
||||
|
||||
# ESLint file cache (npm run lint --cache / complexity ratchets)
|
||||
@@ -251,8 +245,6 @@ _artifacts/
|
||||
|
||||
# CI/local quality artifacts (eslint-results.json, quality-ratchet.md, etc.)
|
||||
.artifacts/
|
||||
# Isolated Devin bridge workspaces, evidence, and test databases
|
||||
.sandbox/
|
||||
|
||||
# Homologation E2E suite (npm run homolog) — real-environment credentials + report output
|
||||
.env.homolog
|
||||
@@ -261,11 +253,7 @@ tests/homolog/ui/.auth/
|
||||
homolog-report/
|
||||
docker-compose.yml.bak
|
||||
.playwright-cli/
|
||||
# Playwright screenshot/log output. Today every artifact happens to land inside
|
||||
# output/**/.playwright-cli/ (covered above), but anything written directly to
|
||||
# output/ would otherwise show up as untracked.
|
||||
/output/
|
||||
|
||||
# _tasks e um repo git SEPARADO (ver AGENTS.md). _tasks/ (com barra) NAO ignora um
|
||||
# SYMLINK _tasks; /_tasks (ancorado) cobre symlink/dir na raiz (incidente 2026-08-08).
|
||||
/_tasks
|
||||
# Runtime artifacts
|
||||
.chroma_db/
|
||||
omniroute.log
|
||||
|
||||
20
.state/session.md
Normal file
20
.state/session.md
Normal file
@@ -0,0 +1,20 @@
|
||||
- Session initialized at Tue Aug 4 00:05:16 CEST 2026
|
||||
- Session initialized at Tue Aug 4 00:05:30 CEST 2026
|
||||
- Session initialized at Tue Aug 4 00:06:20 CEST 2026
|
||||
- Session initialized at Thu Aug 6 20:06:52 CEST 2026
|
||||
- Session initialized at Thu Aug 6 20:35:48 CEST 2026
|
||||
- Session initialized at Thu Aug 6 20:39:00 CEST 2026
|
||||
- Session initialized at Thu Aug 6 21:04:02 CEST 2026
|
||||
- Session initialized at Thu Aug 6 21:42:52 CEST 2026
|
||||
- Session initialized at Thu Aug 6 22:49:37 CEST 2026
|
||||
- Session initialized at Thu Aug 6 22:50:20 CEST 2026
|
||||
- Session initialized at Thu Aug 6 23:17:04 CEST 2026
|
||||
- Session initialized at Fri Aug 7 05:50:47 CEST 2026
|
||||
- Session initialized at Fri Aug 7 05:57:54 CEST 2026
|
||||
- Session initialized at Fri Aug 7 06:58:16 CEST 2026
|
||||
- Session initialized at Fri Aug 7 07:02:53 CEST 2026
|
||||
- Session initialized at Fri Aug 7 07:33:32 CEST 2026
|
||||
- Session initialized at Fri Aug 7 08:04:36 CEST 2026
|
||||
- Session initialized at Fri Aug 7 08:52:52 CEST 2026
|
||||
- Session initialized at Fri Aug 7 09:01:19 CEST 2026
|
||||
- Session initialized at Sat Aug 8 18:46:20 CEST 2026
|
||||
@@ -1033,11 +1033,7 @@ export const OmniRoutePlugin: Plugin = async (_input, options) => {
|
||||
// Config hook: keep existing catalog shim, and register slash command
|
||||
// templates that ask the agent to call the force-sync tool (OpenCode has no
|
||||
// Pi-style registerCommand API; tools + command templates are the native path).
|
||||
const baseConfigHook = createOmniRouteConfigHook(resolved, {
|
||||
cache: sharedCache,
|
||||
diskSnapshotReader: defaultDiskSnapshotReader,
|
||||
diskSnapshotWriter: defaultDiskSnapshotWriter,
|
||||
});
|
||||
const baseConfigHook = createOmniRouteConfigHook(resolved, { cache: sharedCache });
|
||||
const configWithSyncCommand = async (input: Config) => {
|
||||
await baseConfigHook(input);
|
||||
const cfg = input as Config & {
|
||||
@@ -4745,7 +4741,7 @@ export type OmniRouteDiskSnapshotWriter = (
|
||||
export type OmniRouteDiskSnapshotReader = (
|
||||
providerId: string,
|
||||
identityFingerprint: string
|
||||
) => Promise<(Omit<OmniRouteFetchCacheEntry, "expiresAt"> & { writtenAt?: number }) | undefined>;
|
||||
) => Promise<Omit<OmniRouteFetchCacheEntry, "expiresAt"> | undefined>;
|
||||
|
||||
/**
|
||||
* Bind a snapshot to the endpoint and effective credential tuple without
|
||||
@@ -4828,36 +4824,15 @@ export const defaultDiskSnapshotReader: OmniRouteDiskSnapshotReader = async (
|
||||
? parsed.rawCompressionCombos
|
||||
: [],
|
||||
rawConnections: Array.isArray(parsed.rawConnections) ? parsed.rawConnections : [],
|
||||
writtenAt: typeof parsed.writtenAt === "number" ? parsed.writtenAt : undefined,
|
||||
};
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
/** No-op disk-cache pair — used by tests to avoid filesystem side effects.
|
||||
* Also used as the default in createOmniRouteConfigHook so that tests
|
||||
* that don't pass a diskSnapshotReader don't read real snapshot files
|
||||
* from the user's ~/.local/share/opencode/plugins/ directory.
|
||||
* The OmniRoutePlugin function passes the real defaultDiskSnapshotReader
|
||||
* explicitly. */
|
||||
export const noopDiskSnapshotReader: OmniRouteDiskSnapshotReader = async () => undefined;
|
||||
/** No-op disk-cache pair — used by tests to avoid filesystem side effects. */
|
||||
export const noopDiskSnapshotWriter: OmniRouteDiskSnapshotWriter = async () => {};
|
||||
|
||||
/**
|
||||
* In-flight refresh guard: prevents concurrent refreshes for the same
|
||||
* cacheKey. When a warm snapshot is served, the refresh runs detached; if
|
||||
* a second hook invocation arrives before the refresh completes, it should
|
||||
* piggyback on the in-flight promise rather than starting a second one.
|
||||
* Cleared on settle so it doesn't leak.
|
||||
*/
|
||||
const _inflightRefresh: Map<string, Promise<void>> = new Map();
|
||||
|
||||
/** Reset the in-flight refresh guard (for test isolation). */
|
||||
export function _resetInflightRefresh(): void {
|
||||
_inflightRefresh.clear();
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// Debug logging (features.debugLog)
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
@@ -5092,6 +5067,7 @@ export function createDebugLoggingFetch(
|
||||
}
|
||||
};
|
||||
}
|
||||
export const noopDiskSnapshotReader: OmniRouteDiskSnapshotReader = async () => undefined;
|
||||
|
||||
export type OmniRouteReadAuthJson = () => Promise<AuthJsonShape | undefined | null>;
|
||||
|
||||
@@ -5194,8 +5170,8 @@ export function createOmniRouteConfigHook(
|
||||
const compressionMetaFetcher =
|
||||
deps.compressionMetaFetcher ?? defaultOmniRouteCompressionMetaFetcher;
|
||||
const providersFetcher = deps.providersFetcher ?? defaultOmniRouteProvidersFetcher;
|
||||
const diskSnapshotReader = deps.diskSnapshotReader ?? noopDiskSnapshotReader;
|
||||
const diskSnapshotWriter = deps.diskSnapshotWriter ?? noopDiskSnapshotWriter;
|
||||
const diskSnapshotReader = deps.diskSnapshotReader ?? defaultDiskSnapshotReader;
|
||||
const diskSnapshotWriter = deps.diskSnapshotWriter ?? defaultDiskSnapshotWriter;
|
||||
const now = deps.now ?? Date.now;
|
||||
const cache: OmniRouteFetchCache = deps.cache ?? new Map();
|
||||
const logger = deps.logger ?? console;
|
||||
@@ -5290,12 +5266,12 @@ export function createOmniRouteConfigHook(
|
||||
const t = now();
|
||||
const cached = cache.get(cacheKey);
|
||||
|
||||
let rawModels: OmniRouteRawModelEntry[] = [];
|
||||
let rawCombos: OmniRouteRawCombo[] = [];
|
||||
let rawAutoCombos: OmniRouteRawAutoCombo[] = [];
|
||||
let rawEnrichment: OmniRouteEnrichmentMap = new Map();
|
||||
let rawCompressionCombos: OmniRouteCompressionCombo[] = [];
|
||||
let rawConnections: OmniRouteProviderConnection[] = [];
|
||||
let rawModels: OmniRouteRawModelEntry[];
|
||||
let rawCombos: OmniRouteRawCombo[];
|
||||
let rawAutoCombos: OmniRouteRawAutoCombo[];
|
||||
let rawEnrichment: OmniRouteEnrichmentMap;
|
||||
let rawCompressionCombos: OmniRouteCompressionCombo[];
|
||||
let rawConnections: OmniRouteProviderConnection[];
|
||||
|
||||
if (cached && cached.expiresAt > t) {
|
||||
rawModels = cached.rawModels;
|
||||
@@ -5305,275 +5281,160 @@ export function createOmniRouteConfigHook(
|
||||
rawCompressionCombos = cached.rawCompressionCombos;
|
||||
rawConnections = cached.rawConnections;
|
||||
} else {
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// Warm startup: read the disk snapshot before fetching so the provider
|
||||
// registers immediately with the last-known-good catalog. The live
|
||||
// fetch then refreshes in the background (detached) and updates the
|
||||
// cache + snapshot. Gated by features.diskCache (default-on).
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
let warmSnapshot: Omit<OmniRouteFetchCacheEntry, "expiresAt"> | undefined;
|
||||
if (wantDiskCache) {
|
||||
const snapshotResult = await diskSnapshotReader(resolved.providerId, snapshotFingerprint);
|
||||
if (snapshotResult && snapshotResult.rawModels.length > 0) {
|
||||
warmSnapshot = snapshotResult;
|
||||
// Log snapshot age (accept any age — instant beats empty).
|
||||
const age = (snapshotResult as { writtenAt?: number }).writtenAt;
|
||||
const ageLabel = typeof age === "number" ? `${Math.round((Date.now() - age) / 3_600_000)}h` : "unknown";
|
||||
// Fail-open fetcher errors: on /v1/models throw, fall back to empty
|
||||
// catalog (still publish a stub block so OC has a complete-shape
|
||||
// entry); on /api/combos throw, publish models-only. Disk-cache
|
||||
// fallback below recovers the last-known-good catalog when the
|
||||
// fetcher threw (network down / 403 / timeout) AND features.diskCache
|
||||
// !== false. A 0-entry SUCCESS (fresh tenant) does NOT trigger
|
||||
// disk fallback — that's a valid empty catalog.
|
||||
let modelsFetchThrew = false;
|
||||
try {
|
||||
rawModels = await fetcher(baseURL, apiKey, 10_000);
|
||||
} catch (err) {
|
||||
logger.warn(
|
||||
"[omniroute-plugin] config shim: /v1/models fetch failed; publishing stub provider entry",
|
||||
err
|
||||
);
|
||||
rawModels = [];
|
||||
modelsFetchThrew = true;
|
||||
}
|
||||
const modelsFetchOk = !modelsFetchThrew && rawModels.length > 0;
|
||||
|
||||
rawCombos = [];
|
||||
try {
|
||||
rawCombos = await combosFetcher(baseURL, managementReadToken, 10_000);
|
||||
} catch (err) {
|
||||
logger.warn(
|
||||
"[omniroute-plugin] config shim: /api/combos fetch failed; publishing models-only static catalog",
|
||||
err
|
||||
);
|
||||
}
|
||||
|
||||
rawAutoCombos = [];
|
||||
if (wantAutoCombos) {
|
||||
try {
|
||||
rawAutoCombos = await autoCombosFetcher(baseURL, managementReadToken, 5_000);
|
||||
} catch {
|
||||
// Already handled inside the default fetcher
|
||||
}
|
||||
}
|
||||
|
||||
// Eagerly fetch enrichment so the static block can overlay human
|
||||
// display names on raw model ids. On OC ≤1.15.5 the dynamic
|
||||
// `provider.models` hook never fires in `serve` mode, so the static
|
||||
// block IS what reaches `/provider` and the TUI model picker.
|
||||
// Gated by `features.enrichment` (default-on). Soft-fail on error —
|
||||
// we still publish a name-less catalog if /api/pricing/models is
|
||||
// unreachable.
|
||||
rawEnrichment = new Map();
|
||||
if (wantEnrichment) {
|
||||
try {
|
||||
rawEnrichment = await enrichmentFetcher(baseURL, managementReadToken, 10_000);
|
||||
} catch (err) {
|
||||
logger.warn(
|
||||
`[omniroute-plugin] config shim: warm startup from disk snapshot (${snapshotResult.rawModels.length} models, age ${ageLabel})`
|
||||
"[omniroute-plugin] config shim: /api/pricing/models fetch failed; publishing raw-id static catalog",
|
||||
err
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// Parallel refresh: all six fetchers run concurrently via
|
||||
// Promise.allSettled. Each wrapper never rejects (catches internally)
|
||||
// so partial failure is tolerated — same soft-fail semantics as the
|
||||
// old sequential chain, but ~6x faster.
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
const doRefresh = async (): Promise<void> => {
|
||||
let modelsFetchThrew = false;
|
||||
let localRawModels: OmniRouteRawModelEntry[] = [];
|
||||
let localRawCombos: OmniRouteRawCombo[] = [];
|
||||
let localRawAutoCombos: OmniRouteRawAutoCombo[] = [];
|
||||
let localRawEnrichment: OmniRouteEnrichmentMap = new Map();
|
||||
let localRawCompressionCombos: OmniRouteCompressionCombo[] = [];
|
||||
let localRawConnections: OmniRouteProviderConnection[] = [];
|
||||
|
||||
// Each wrapper keeps the existing try/catch, default value, and
|
||||
// exact warn message so per-endpoint fallbacks are preserved.
|
||||
const doModels = async (): Promise<void> => {
|
||||
try {
|
||||
localRawModels = await fetcher(baseURL, apiKey, 10_000);
|
||||
} catch (err) {
|
||||
logger.warn(
|
||||
"[omniroute-plugin] config shim: /v1/models fetch failed; publishing stub provider entry",
|
||||
err
|
||||
);
|
||||
localRawModels = [];
|
||||
modelsFetchThrew = true;
|
||||
}
|
||||
};
|
||||
|
||||
const doCombos = async (): Promise<void> => {
|
||||
try {
|
||||
localRawCombos = await combosFetcher(baseURL, managementReadToken, 10_000);
|
||||
} catch (err) {
|
||||
logger.warn(
|
||||
"[omniroute-plugin] config shim: /api/combos fetch failed; publishing models-only static catalog",
|
||||
err
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const doAutoCombos = async (): Promise<void> => {
|
||||
if (!wantAutoCombos) return;
|
||||
try {
|
||||
localRawAutoCombos = await autoCombosFetcher(baseURL, managementReadToken, 5_000);
|
||||
} catch {
|
||||
// Already handled inside the default fetcher
|
||||
}
|
||||
};
|
||||
|
||||
const doEnrichment = async (): Promise<void> => {
|
||||
if (!wantEnrichment) return;
|
||||
try {
|
||||
localRawEnrichment = await enrichmentFetcher(baseURL, managementReadToken, 10_000);
|
||||
} catch (err) {
|
||||
logger.warn(
|
||||
"[omniroute-plugin] config shim: /api/pricing/models fetch failed; publishing raw-id static catalog",
|
||||
err
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const doCompression = async (): Promise<void> => {
|
||||
if (!wantCompressionMeta) return;
|
||||
try {
|
||||
localRawCompressionCombos = await compressionMetaFetcher(baseURL, managementReadToken, 10_000);
|
||||
} catch (err) {
|
||||
logger.warn(
|
||||
"[omniroute-plugin] config shim: /api/context/combos fetch failed; publishing combos without compression suffix",
|
||||
err
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const doConnections = async (): Promise<void> => {
|
||||
if (!wantUsableOnly) return;
|
||||
try {
|
||||
localRawConnections = await providersFetcher(baseURL, managementReadToken, 10_000);
|
||||
} catch (err) {
|
||||
logger.warn(
|
||||
"[omniroute-plugin] config shim: /api/providers fetch failed; usableOnly filter disabled for this refresh",
|
||||
err
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
await Promise.allSettled([
|
||||
doModels(),
|
||||
doCombos(),
|
||||
doAutoCombos(),
|
||||
doEnrichment(),
|
||||
doCompression(),
|
||||
doConnections(),
|
||||
]);
|
||||
|
||||
const modelsFetchOk = !modelsFetchThrew && localRawModels.length > 0;
|
||||
|
||||
// Disk-cache fallback (cold first run, no warm snapshot): when the
|
||||
// live fetch returned no models AND features.diskCache !== false,
|
||||
// hydrate from the last-known-good snapshot so OC still surfaces a
|
||||
// usable catalog (e.g. IP whitelist drop, offline laptop).
|
||||
if (modelsFetchThrew && wantDiskCache && !warmSnapshot) {
|
||||
const snapshot = await diskSnapshotReader(resolved.providerId, snapshotFingerprint);
|
||||
if (snapshot && snapshot.rawModels.length > 0) {
|
||||
logger.warn(
|
||||
`[omniroute-plugin] config shim: /v1/models unreachable; using stale disk cache (${snapshot.rawModels.length} models)`
|
||||
);
|
||||
localRawModels = snapshot.rawModels;
|
||||
localRawCombos = snapshot.rawCombos;
|
||||
localRawAutoCombos = snapshot.rawAutoCombos ?? [];
|
||||
localRawEnrichment = snapshot.rawEnrichment;
|
||||
localRawCompressionCombos = snapshot.rawCompressionCombos;
|
||||
localRawConnections = snapshot.rawConnections;
|
||||
}
|
||||
// Compression-metadata fetch — opt-in via features.compressionMetadata.
|
||||
// When on, the default pipeline is appended to every combo `name` so
|
||||
// the TUI picker advertises which compression a combo applies.
|
||||
rawCompressionCombos = [];
|
||||
if (wantCompressionMeta) {
|
||||
try {
|
||||
rawCompressionCombos = await compressionMetaFetcher(baseURL, managementReadToken, 10_000);
|
||||
} catch (err) {
|
||||
logger.warn(
|
||||
"[omniroute-plugin] config shim: /api/context/combos fetch failed; publishing combos without compression suffix",
|
||||
err
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Cache even partial results — a subsequent provider-hook call should
|
||||
// not re-burn the timeout window on the same broken endpoint.
|
||||
cache.set(cacheKey, {
|
||||
rawModels: localRawModels,
|
||||
rawCombos: localRawCombos,
|
||||
rawAutoCombos: localRawAutoCombos,
|
||||
rawEnrichment: localRawEnrichment,
|
||||
rawCompressionCombos: localRawCompressionCombos,
|
||||
rawConnections: localRawConnections,
|
||||
expiresAt: now() + resolved.modelCacheTtl,
|
||||
// Provider-connections fetch — opt-in via features.usableOnly. When
|
||||
// on, the static catalog filters out models/combos whose canonical
|
||||
// provider has no active connection. Soft-fail (empty list) disables
|
||||
// the filter for this refresh, never hiding the whole catalog.
|
||||
rawConnections = [];
|
||||
if (wantUsableOnly) {
|
||||
try {
|
||||
rawConnections = await providersFetcher(baseURL, managementReadToken, 10_000);
|
||||
} catch (err) {
|
||||
logger.warn(
|
||||
"[omniroute-plugin] config shim: /api/providers fetch failed; usableOnly filter disabled for this refresh",
|
||||
err
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Disk-cache fallback: when the live fetch returned no models AND
|
||||
// features.diskCache !== false, hydrate from the last-known-good
|
||||
// snapshot so OC still surfaces a usable catalog (e.g. IP whitelist
|
||||
// drop, offline laptop). The snapshot is whatever we last wrote on
|
||||
// a healthy refresh; staleness is bounded only by how recently the
|
||||
// user was online.
|
||||
if (modelsFetchThrew && wantDiskCache) {
|
||||
const snapshot = await diskSnapshotReader(resolved.providerId, snapshotFingerprint);
|
||||
if (snapshot && snapshot.rawModels.length > 0) {
|
||||
logger.warn(
|
||||
`[omniroute-plugin] config shim: /v1/models unreachable; using stale disk cache (${snapshot.rawModels.length} models)`
|
||||
);
|
||||
rawModels = snapshot.rawModels;
|
||||
rawCombos = snapshot.rawCombos;
|
||||
rawAutoCombos = snapshot.rawAutoCombos ?? [];
|
||||
rawEnrichment = snapshot.rawEnrichment;
|
||||
rawCompressionCombos = snapshot.rawCompressionCombos;
|
||||
rawConnections = snapshot.rawConnections;
|
||||
}
|
||||
}
|
||||
|
||||
// Cache even partial results — a subsequent provider-hook call should
|
||||
// not re-burn the timeout window on the same broken endpoint.
|
||||
cache.set(cacheKey, {
|
||||
rawModels,
|
||||
rawCombos,
|
||||
rawAutoCombos,
|
||||
rawEnrichment,
|
||||
rawCompressionCombos,
|
||||
rawConnections,
|
||||
expiresAt: t + resolved.modelCacheTtl,
|
||||
});
|
||||
|
||||
// Startup diagnostics (file-based) — fires at startup via config hook
|
||||
if (resolved.features?.startupDebug === true) {
|
||||
await writeStartupDiagnostics({
|
||||
providerId: resolved.providerId,
|
||||
baseURL,
|
||||
modelCount: rawModels.length,
|
||||
comboCount: rawCombos.length,
|
||||
enrichmentSize: rawEnrichment.size,
|
||||
autoComboCount: rawAutoCombos.length,
|
||||
enrichment: rawEnrichment,
|
||||
autoCombos: rawAutoCombos,
|
||||
features: resolved.features,
|
||||
});
|
||||
}
|
||||
|
||||
// Startup diagnostics (file-based) — fires at startup via config hook
|
||||
if (resolved.features?.startupDebug === true) {
|
||||
await writeStartupDiagnostics({
|
||||
providerId: resolved.providerId,
|
||||
baseURL,
|
||||
modelCount: localRawModels.length,
|
||||
comboCount: localRawCombos.length,
|
||||
enrichmentSize: localRawEnrichment.size,
|
||||
autoComboCount: localRawAutoCombos.length,
|
||||
enrichment: localRawEnrichment,
|
||||
autoCombos: localRawAutoCombos,
|
||||
features: resolved.features,
|
||||
});
|
||||
}
|
||||
|
||||
// Disk-cache write: persist the last successful (or any non-empty)
|
||||
// catalog so a subsequent cold start with a failed fetch can recover.
|
||||
// Best-effort; soft-fail keeps us moving when the data dir isn't
|
||||
// writable (e.g. read-only container). A failed refresh never
|
||||
// overwrites the snapshot (modelsFetchOk gate).
|
||||
if (modelsFetchOk && wantDiskCache) {
|
||||
await diskSnapshotWriter(
|
||||
resolved.providerId,
|
||||
{
|
||||
rawModels: localRawModels,
|
||||
rawCombos: localRawCombos,
|
||||
rawAutoCombos: localRawAutoCombos,
|
||||
rawEnrichment: localRawEnrichment,
|
||||
rawCompressionCombos: localRawCompressionCombos,
|
||||
rawConnections: localRawConnections,
|
||||
},
|
||||
snapshotFingerprint
|
||||
);
|
||||
}
|
||||
|
||||
// Re-publish a fresh block via the shared cache so OC >=1.14.49's
|
||||
// dynamic provider hook picks it up from the cache. When the models
|
||||
// fetch threw and a warm snapshot was served, keep the warm block
|
||||
// (no downgrade to stub).
|
||||
if (modelsFetchOk || !warmSnapshot) {
|
||||
const freshBlock = buildStaticProviderEntry(
|
||||
localRawModels,
|
||||
localRawCombos,
|
||||
resolved,
|
||||
baseURL,
|
||||
apiKey,
|
||||
localRawEnrichment,
|
||||
localRawCompressionCombos,
|
||||
localRawConnections,
|
||||
localRawAutoCombos
|
||||
);
|
||||
const inputWithProvider2 = input as { provider?: Record<string, unknown> };
|
||||
if (inputWithProvider2.provider) {
|
||||
inputWithProvider2.provider[resolved.providerId] = freshBlock;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (warmSnapshot) {
|
||||
// Warm startup: publish the snapshot block immediately, then run
|
||||
// the refresh detached (never a floating unhandled rejection).
|
||||
rawModels = warmSnapshot.rawModels;
|
||||
rawCombos = warmSnapshot.rawCombos;
|
||||
rawAutoCombos = warmSnapshot.rawAutoCombos ?? [];
|
||||
rawEnrichment = warmSnapshot.rawEnrichment;
|
||||
rawCompressionCombos = warmSnapshot.rawCompressionCombos;
|
||||
rawConnections = warmSnapshot.rawConnections;
|
||||
|
||||
// In-flight guard: if a refresh is already running for this
|
||||
// cacheKey, piggyback on it instead of starting a second one.
|
||||
const existing = _inflightRefresh.get(cacheKey);
|
||||
if (existing) {
|
||||
// Another refresh is in-flight — don't start a second one.
|
||||
// The existing refresh will update the cache when it completes.
|
||||
} else {
|
||||
const refreshP = doRefresh()
|
||||
.catch((err: unknown) => {
|
||||
logger.warn("[omniroute-plugin] config shim: background refresh failed", err);
|
||||
})
|
||||
.finally(() => {
|
||||
_inflightRefresh.delete(cacheKey);
|
||||
});
|
||||
_inflightRefresh.set(cacheKey, refreshP);
|
||||
}
|
||||
} else {
|
||||
// Cold first run (no warm snapshot): await the refresh so the
|
||||
// first publish is always correct. In-flight guard still applies.
|
||||
const existing = _inflightRefresh.get(cacheKey);
|
||||
if (existing) {
|
||||
await existing;
|
||||
// After the in-flight refresh completes, the cache has the data.
|
||||
const fresh = cache.get(cacheKey);
|
||||
if (fresh) {
|
||||
rawModels = fresh.rawModels;
|
||||
rawCombos = fresh.rawCombos;
|
||||
rawAutoCombos = fresh.rawAutoCombos;
|
||||
rawEnrichment = fresh.rawEnrichment;
|
||||
rawCompressionCombos = fresh.rawCompressionCombos;
|
||||
rawConnections = fresh.rawConnections;
|
||||
}
|
||||
} else {
|
||||
const refreshP = doRefresh()
|
||||
.catch((err: unknown) => {
|
||||
logger.warn("[omniroute-plugin] config shim: refresh failed", err);
|
||||
})
|
||||
.finally(() => {
|
||||
_inflightRefresh.delete(cacheKey);
|
||||
});
|
||||
_inflightRefresh.set(cacheKey, refreshP);
|
||||
await refreshP;
|
||||
// After the refresh, the cache has the data.
|
||||
const fresh = cache.get(cacheKey);
|
||||
if (fresh) {
|
||||
rawModels = fresh.rawModels;
|
||||
rawCombos = fresh.rawCombos;
|
||||
rawAutoCombos = fresh.rawAutoCombos;
|
||||
rawEnrichment = fresh.rawEnrichment;
|
||||
rawCompressionCombos = fresh.rawCompressionCombos;
|
||||
rawConnections = fresh.rawConnections;
|
||||
}
|
||||
}
|
||||
// Disk-cache write: persist the last successful (or any non-empty)
|
||||
// catalog so a subsequent cold start with a failed fetch can recover.
|
||||
// Best-effort; soft-fail keeps us moving when the data dir isn't
|
||||
// writable (e.g. read-only container).
|
||||
if (modelsFetchOk && wantDiskCache) {
|
||||
await diskSnapshotWriter(
|
||||
resolved.providerId,
|
||||
{
|
||||
rawModels,
|
||||
rawCombos,
|
||||
rawAutoCombos,
|
||||
rawEnrichment,
|
||||
rawCompressionCombos,
|
||||
rawConnections,
|
||||
},
|
||||
snapshotFingerprint
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -33,7 +33,6 @@ import {
|
||||
createOmniRouteProviderHook,
|
||||
OmniRoutePlugin,
|
||||
resolveOmniRoutePluginOptions,
|
||||
_resetInflightRefresh,
|
||||
type OmniRouteCombosFetcher,
|
||||
type OmniRouteEnrichmentEntry,
|
||||
type OmniRouteEnrichmentFetcher,
|
||||
@@ -48,16 +47,6 @@ import {
|
||||
type OmniRouteStaticProviderEntry,
|
||||
} from "../src/index.js";
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// Test isolation: reset the module-level in-flight refresh guard between
|
||||
// tests so a detached refresh from a previous test doesn't leak into the
|
||||
// next one.
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
test.beforeEach(() => {
|
||||
_resetInflightRefresh();
|
||||
});
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// Fixtures
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
@@ -1250,10 +1239,7 @@ test("config: diskCache hydrates stale snapshot when /v1/models throws", async (
|
||||
);
|
||||
assert.equal(writes, 0, "disk write skipped when live fetch failed");
|
||||
assert.ok(
|
||||
logger.entries.some((e) =>
|
||||
String(e[0]).includes("using stale disk cache") ||
|
||||
String(e[0]).includes("warm startup from disk snapshot")
|
||||
),
|
||||
logger.entries.some((e) => String(e[0]).includes("using stale disk cache")),
|
||||
"disk-cache hydration breadcrumb emitted"
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,827 +0,0 @@
|
||||
/**
|
||||
* Warm-startup + parallel-refresh tests for the opencode-plugin config shim.
|
||||
*
|
||||
* Covers `createOmniRouteConfigHook(opts, deps)`:
|
||||
* - (a) Warm startup: cache miss + matching snapshot → provider block
|
||||
* populated from snapshot data (not live fetch data).
|
||||
* - (b) Fingerprint mismatch: reader returns undefined → no warm publish,
|
||||
* falls through to awaited fetch (cold-start behavior).
|
||||
* - (c) Successful parallel refresh: all fetchers resolve → cache updated,
|
||||
* disk snapshot written.
|
||||
* - (d) Failed refresh keeps the snapshot: warm-served + models fetcher
|
||||
* rejects → no disk overwrite, block stays at warm-snapshot shape.
|
||||
* - (e) Parallelism: all six fetchers start concurrently (not sequential).
|
||||
* - (f) Soft-fail parity under Promise.allSettled: per-endpoint
|
||||
* fallbacks + logger.warn breadcrumbs preserved.
|
||||
* - (g) No double-refresh: concurrent hook invocations on the same cacheKey
|
||||
* trigger only one refresh (in-flight guard).
|
||||
* - (h) features.diskCache: false disables the warm read entirely.
|
||||
*
|
||||
* Mocking strategy: every dependency is DI-injected at hook construction
|
||||
* (same pattern as config-shim.test.ts). No global monkey-patching.
|
||||
*/
|
||||
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import type { Config } from "@opencode-ai/plugin";
|
||||
|
||||
import {
|
||||
createOmniRouteConfigHook,
|
||||
resolveOmniRoutePluginOptions,
|
||||
_resetInflightRefresh,
|
||||
type OmniRouteAutoCombosFetcher,
|
||||
type OmniRouteCombosFetcher,
|
||||
type OmniRouteCompressionMetaFetcher,
|
||||
type OmniRouteEnrichmentEntry,
|
||||
type OmniRouteEnrichmentFetcher,
|
||||
type OmniRouteEnrichmentMap,
|
||||
type OmniRouteFetchCache,
|
||||
type OmniRouteModelsFetcher,
|
||||
type OmniRouteProviderConnection,
|
||||
type OmniRouteProvidersFetcher,
|
||||
type OmniRouteRawAutoCombo,
|
||||
type OmniRouteRawCombo,
|
||||
type OmniRouteRawModelEntry,
|
||||
type OmniRouteReadAuthJson,
|
||||
type OmniRouteStaticProviderEntry,
|
||||
type OmniRouteDiskSnapshotReader,
|
||||
type OmniRouteDiskSnapshotWriter,
|
||||
type OmniRouteCompressionCombo,
|
||||
} from "../src/index.js";
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// Test isolation: reset the module-level in-flight refresh guard between
|
||||
// tests so a detached refresh from a previous test doesn't leak into the
|
||||
// next one (same cacheKey, different cache instance).
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
test.beforeEach(() => {
|
||||
_resetInflightRefresh();
|
||||
});
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// Fixtures
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
const MODEL_CLAUDE: OmniRouteRawModelEntry = {
|
||||
id: "claude-sonnet-4-6",
|
||||
capabilities: {
|
||||
tool_calling: true,
|
||||
reasoning: true,
|
||||
vision: true,
|
||||
thinking: false,
|
||||
temperature: true,
|
||||
},
|
||||
context_length: 200_000,
|
||||
max_output_tokens: 64_000,
|
||||
max_input_tokens: 180_000,
|
||||
input_modalities: ["text", "image"],
|
||||
output_modalities: ["text"],
|
||||
};
|
||||
|
||||
const MODEL_GEMINI: OmniRouteRawModelEntry = {
|
||||
id: "gemini-3-flash",
|
||||
capabilities: { tool_calling: true, reasoning: false, vision: true, thinking: false },
|
||||
context_length: 1_000_000,
|
||||
max_output_tokens: 8_192,
|
||||
input_modalities: ["text", "image"],
|
||||
output_modalities: ["text"],
|
||||
};
|
||||
|
||||
const COMBO_CLAUDE_TIER: OmniRouteRawCombo = {
|
||||
id: "combo-claude-tier",
|
||||
name: "Claude Tier",
|
||||
models: [
|
||||
{ id: "s1", kind: "model", model: "claude-sonnet-4-6", weight: 100 },
|
||||
{ id: "s2", kind: "model", model: "gemini-3-flash", weight: 50 },
|
||||
],
|
||||
};
|
||||
|
||||
const AUTO_COMBO: OmniRouteRawAutoCombo = {
|
||||
id: "auto",
|
||||
name: "Auto",
|
||||
};
|
||||
|
||||
const COMPRESSION_COMBO: OmniRouteCompressionCombo = {
|
||||
id: "ctx-combo-1",
|
||||
name: "Context Combo",
|
||||
pipeline: "gzip",
|
||||
};
|
||||
|
||||
const CONNECTION_CLAUDE: OmniRouteProviderConnection = {
|
||||
id: "c1",
|
||||
provider: "claude",
|
||||
isActive: true,
|
||||
testStatus: "active",
|
||||
};
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// DI stub helpers
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
function stubReadAuthJson(
|
||||
value: Record<string, unknown> | undefined | null
|
||||
): OmniRouteReadAuthJson {
|
||||
return async () => value as never;
|
||||
}
|
||||
|
||||
function immediateFetcher<T extends (...args: unknown[]) => Promise<unknown>>(
|
||||
payload: ReturnType<T> extends Promise<infer U> ? U : never
|
||||
): T & { callCount: () => number; startedAt: () => number | undefined } {
|
||||
let n = 0;
|
||||
let start: number | undefined;
|
||||
const f = async (..._args: unknown[]) => {
|
||||
start = Date.now();
|
||||
n++;
|
||||
return payload;
|
||||
};
|
||||
return Object.assign(f as T, { callCount: () => n, startedAt: () => start });
|
||||
}
|
||||
|
||||
function throwingFetcher<T extends (...args: unknown[]) => Promise<unknown>>(
|
||||
msg = "ECONNREFUSED"
|
||||
): T & { callCount: () => number } {
|
||||
let n = 0;
|
||||
const f = async (..._args: unknown[]) => {
|
||||
n++;
|
||||
throw new Error(msg);
|
||||
};
|
||||
return Object.assign(f as T, { callCount: () => n });
|
||||
}
|
||||
|
||||
interface WarnCapture {
|
||||
warn: (...args: unknown[]) => void;
|
||||
entries: unknown[][];
|
||||
}
|
||||
|
||||
function captureWarn(): WarnCapture {
|
||||
const entries: unknown[][] = [];
|
||||
return {
|
||||
warn: (...args: unknown[]) => {
|
||||
entries.push(args);
|
||||
},
|
||||
entries,
|
||||
};
|
||||
}
|
||||
|
||||
function makeInput(initialProvider: Record<string, unknown> = {}): Config {
|
||||
return { provider: initialProvider } as unknown as Config;
|
||||
}
|
||||
|
||||
/** Build a valid auth.json stub for the default providerId. */
|
||||
function authStub() {
|
||||
return stubReadAuthJson({
|
||||
"opencode-omniroute": {
|
||||
type: "api",
|
||||
key: "sk-test",
|
||||
baseURL: "https://or.example.com/v1",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// (a) Warm startup: cache miss + matching snapshot → provider block populated
|
||||
// from snapshot data (not live fetch data)
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
test("warm-startup: snapshot data used when snapshot is present", async () => {
|
||||
// Live fetch returns MODEL_CLAUDE, but snapshot has MODEL_GEMINI.
|
||||
// With warm startup, the block should contain the snapshot data.
|
||||
const fetcher = immediateFetcher<OmniRouteModelsFetcher>([MODEL_CLAUDE]);
|
||||
const combosFetcher = immediateFetcher<OmniRouteCombosFetcher>([]);
|
||||
const autoCombosFetcher = immediateFetcher<OmniRouteAutoCombosFetcher>([]);
|
||||
const enrichmentFetcher = immediateFetcher<OmniRouteEnrichmentFetcher>(new Map());
|
||||
const compressionMetaFetcher = immediateFetcher<OmniRouteCompressionMetaFetcher>([]);
|
||||
const providersFetcher = immediateFetcher<OmniRouteProvidersFetcher>([]);
|
||||
const logger = captureWarn();
|
||||
|
||||
const snapshot: Omit<import("../src/index.js").OmniRouteFetchCacheEntry, "expiresAt"> = {
|
||||
rawModels: [MODEL_GEMINI],
|
||||
rawCombos: [],
|
||||
rawAutoCombos: [],
|
||||
rawEnrichment: new Map(),
|
||||
rawCompressionCombos: [],
|
||||
rawConnections: [],
|
||||
};
|
||||
|
||||
const diskSnapshotReader: OmniRouteDiskSnapshotReader = async () => snapshot;
|
||||
const diskSnapshotWriter: OmniRouteDiskSnapshotWriter = async () => {};
|
||||
|
||||
const hook = createOmniRouteConfigHook(
|
||||
{ providerId: "omniroute" },
|
||||
{
|
||||
readAuthJson: authStub(),
|
||||
fetcher,
|
||||
combosFetcher,
|
||||
autoCombosFetcher,
|
||||
enrichmentFetcher,
|
||||
compressionMetaFetcher,
|
||||
providersFetcher,
|
||||
diskSnapshotReader,
|
||||
diskSnapshotWriter,
|
||||
logger,
|
||||
}
|
||||
);
|
||||
|
||||
const input = makeInput();
|
||||
await hook(input);
|
||||
|
||||
const provider = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider;
|
||||
const entry = provider["opencode-omniroute"];
|
||||
assert.ok(entry, "provider entry published");
|
||||
|
||||
// With warm startup, the block should contain the snapshot data (GEMINI),
|
||||
// not the live fetch data (CLAUDE). This is the key assertion: the warm
|
||||
// snapshot is served first, and the live refresh updates the cache in the
|
||||
// background. On the next hook invocation, the cache will have the fresh data.
|
||||
const hasGemini = entry.models["opencode-omniroute/gemini-3-flash"] !== undefined;
|
||||
const hasClaude = entry.models["opencode-omniroute/claude-sonnet-4-6"] !== undefined;
|
||||
assert.ok(
|
||||
hasGemini || hasClaude,
|
||||
"provider block has at least one model"
|
||||
);
|
||||
|
||||
// The warm-startup breadcrumb should be emitted.
|
||||
assert.ok(
|
||||
logger.entries.some((e) =>
|
||||
String(e[0]).includes("warm startup from disk snapshot")
|
||||
),
|
||||
"warm-startup breadcrumb emitted"
|
||||
);
|
||||
});
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// (b) Fingerprint mismatch: reader returns undefined → no warm publish,
|
||||
// falls through to awaited fetch
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
test("warm-startup: fingerprint mismatch → no warm publish, awaited fetch", async () => {
|
||||
const fetcher = immediateFetcher<OmniRouteModelsFetcher>([MODEL_CLAUDE]);
|
||||
const combosFetcher = immediateFetcher<OmniRouteCombosFetcher>([]);
|
||||
const logger = captureWarn();
|
||||
|
||||
// Reader returns undefined → fingerprint mismatch or missing snapshot.
|
||||
const diskSnapshotReader: OmniRouteDiskSnapshotReader = async () => undefined;
|
||||
const diskSnapshotWriter: OmniRouteDiskSnapshotWriter = async () => {};
|
||||
|
||||
const hook = createOmniRouteConfigHook(
|
||||
{ providerId: "omniroute" },
|
||||
{
|
||||
readAuthJson: authStub(),
|
||||
fetcher,
|
||||
combosFetcher,
|
||||
diskSnapshotReader,
|
||||
diskSnapshotWriter,
|
||||
logger,
|
||||
}
|
||||
);
|
||||
|
||||
const input = makeInput();
|
||||
await hook(input);
|
||||
|
||||
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider[
|
||||
"opencode-omniroute"
|
||||
];
|
||||
assert.ok(entry, "provider entry published from live fetch");
|
||||
// Live fetch data, not snapshot data.
|
||||
assert.ok(
|
||||
entry.models["opencode-omniroute/claude-sonnet-4-6"],
|
||||
"live fetch model present"
|
||||
);
|
||||
assert.equal(fetcher.callCount(), 1, "fetcher was called (awaited cold path)");
|
||||
// No warm-startup breadcrumb when no snapshot.
|
||||
assert.ok(
|
||||
!logger.entries.some((e) =>
|
||||
String(e[0]).includes("warm startup from disk snapshot")
|
||||
),
|
||||
"no warm-startup breadcrumb when no snapshot"
|
||||
);
|
||||
});
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// (c) Successful parallel refresh: all fetchers resolve → cache updated,
|
||||
// disk snapshot written, block re-published with fresh data
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
test("warm-startup: parallel refresh updates cache + writes snapshot", async () => {
|
||||
const fetcher = immediateFetcher<OmniRouteModelsFetcher>([MODEL_CLAUDE]);
|
||||
const combosFetcher = immediateFetcher<OmniRouteCombosFetcher>([COMBO_CLAUDE_TIER]);
|
||||
const autoCombosFetcher = immediateFetcher<OmniRouteAutoCombosFetcher>([AUTO_COMBO]);
|
||||
const enrichmentFetcher = immediateFetcher<OmniRouteEnrichmentFetcher>(
|
||||
new Map<string, OmniRouteEnrichmentEntry>([
|
||||
["claude-sonnet-4-6", { name: "Claude Sonnet 4.6" }],
|
||||
])
|
||||
);
|
||||
const compressionMetaFetcher = immediateFetcher<OmniRouteCompressionMetaFetcher>([
|
||||
COMPRESSION_COMBO,
|
||||
]);
|
||||
const providersFetcher = immediateFetcher<OmniRouteProvidersFetcher>([CONNECTION_CLAUDE]);
|
||||
const logger = captureWarn();
|
||||
|
||||
const snapshot: Omit<import("../src/index.js").OmniRouteFetchCacheEntry, "expiresAt"> = {
|
||||
rawModels: [MODEL_GEMINI],
|
||||
rawCombos: [],
|
||||
rawAutoCombos: [],
|
||||
rawEnrichment: new Map(),
|
||||
rawCompressionCombos: [],
|
||||
rawConnections: [],
|
||||
};
|
||||
|
||||
const diskSnapshotReader: OmniRouteDiskSnapshotReader = async () => snapshot;
|
||||
let snapshotWrites = 0;
|
||||
const diskSnapshotWriter: OmniRouteDiskSnapshotWriter = async () => {
|
||||
snapshotWrites++;
|
||||
};
|
||||
|
||||
const sharedCache: OmniRouteFetchCache = new Map();
|
||||
|
||||
const hook = createOmniRouteConfigHook(
|
||||
{ providerId: "omniroute", modelCacheTtl: 60_000 },
|
||||
{
|
||||
readAuthJson: authStub(),
|
||||
fetcher,
|
||||
combosFetcher,
|
||||
autoCombosFetcher,
|
||||
enrichmentFetcher,
|
||||
compressionMetaFetcher,
|
||||
providersFetcher,
|
||||
diskSnapshotReader,
|
||||
diskSnapshotWriter,
|
||||
cache: sharedCache,
|
||||
logger,
|
||||
}
|
||||
);
|
||||
|
||||
const input = makeInput();
|
||||
await hook(input);
|
||||
|
||||
// Warm block should have been published.
|
||||
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider[
|
||||
"opencode-omniroute"
|
||||
];
|
||||
assert.ok(entry, "warm provider entry published");
|
||||
|
||||
// Give detached refresh time to complete.
|
||||
await new Promise((r) => setTimeout(r, 100));
|
||||
|
||||
// After parallel refresh, the cache should have the fresh data.
|
||||
const cacheKey = Array.from(sharedCache.keys())[0];
|
||||
assert.ok(cacheKey, "cache entry created");
|
||||
const cached = sharedCache.get(cacheKey)!;
|
||||
assert.ok(cached.expiresAt > 0, "cache entry has expiresAt");
|
||||
// Fresh data from the live fetchers (not the stale snapshot).
|
||||
assert.equal(cached.rawModels.length, 1, "cache has fresh models");
|
||||
assert.equal(cached.rawModels[0].id, "claude-sonnet-4-6", "cache has correct model");
|
||||
|
||||
// Disk snapshot should have been written.
|
||||
assert.equal(snapshotWrites, 1, "disk snapshot written after successful refresh");
|
||||
});
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// (d) Failed refresh keeps the snapshot: warm-served + models fetcher
|
||||
// rejects → no disk overwrite, block stays at warm-snapshot shape
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
test("warm-startup: failed refresh keeps the snapshot, no disk overwrite", async () => {
|
||||
const fetcher = throwingFetcher<OmniRouteModelsFetcher>();
|
||||
const combosFetcher = throwingFetcher<OmniRouteCombosFetcher>();
|
||||
const logger = captureWarn();
|
||||
|
||||
const snapshot: Omit<import("../src/index.js").OmniRouteFetchCacheEntry, "expiresAt"> = {
|
||||
rawModels: [MODEL_GEMINI],
|
||||
rawCombos: [COMBO_CLAUDE_TIER],
|
||||
rawAutoCombos: [],
|
||||
rawEnrichment: new Map(),
|
||||
rawCompressionCombos: [],
|
||||
rawConnections: [],
|
||||
};
|
||||
|
||||
const diskSnapshotReader: OmniRouteDiskSnapshotReader = async () => snapshot;
|
||||
let snapshotWrites = 0;
|
||||
const diskSnapshotWriter: OmniRouteDiskSnapshotWriter = async () => {
|
||||
snapshotWrites++;
|
||||
};
|
||||
|
||||
const hook = createOmniRouteConfigHook(
|
||||
{ providerId: "omniroute" },
|
||||
{
|
||||
readAuthJson: authStub(),
|
||||
fetcher,
|
||||
combosFetcher,
|
||||
diskSnapshotReader,
|
||||
diskSnapshotWriter,
|
||||
logger,
|
||||
}
|
||||
);
|
||||
|
||||
const input = makeInput();
|
||||
await hook(input);
|
||||
|
||||
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider[
|
||||
"opencode-omniroute"
|
||||
];
|
||||
assert.ok(entry, "warm provider entry published");
|
||||
|
||||
// The block should contain the warm snapshot data (gemini), not be
|
||||
// downgraded to a stub.
|
||||
assert.ok(
|
||||
entry.models["opencode-omniroute/gemini-3-flash"],
|
||||
"warm snapshot model preserved (not downgraded to stub)"
|
||||
);
|
||||
|
||||
// Give detached refresh time to complete.
|
||||
await new Promise((r) => setTimeout(r, 100));
|
||||
|
||||
// No disk write on failed refresh.
|
||||
assert.equal(snapshotWrites, 0, "no disk snapshot written when models fetch failed");
|
||||
});
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// (e) Parallelism: all six fetchers start concurrently (not sequential)
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
test("warm-startup: all fetchers start concurrently (parallel fan-out)", async () => {
|
||||
const startTimes: number[] = [];
|
||||
const barrier = new Promise<void>((r) => {
|
||||
setTimeout(r, 30);
|
||||
});
|
||||
|
||||
function instrumentedFetcher<T extends (...args: unknown[]) => Promise<unknown>>(
|
||||
payload: ReturnType<T> extends Promise<infer U> ? U : never
|
||||
): T & { callCount: () => number } {
|
||||
let n = 0;
|
||||
const f = async (..._args: unknown[]) => {
|
||||
startTimes.push(Date.now());
|
||||
n++;
|
||||
await barrier;
|
||||
return payload;
|
||||
};
|
||||
return Object.assign(f as T, { callCount: () => n });
|
||||
}
|
||||
|
||||
const fetcher = instrumentedFetcher<OmniRouteModelsFetcher>([MODEL_CLAUDE]);
|
||||
const combosFetcher = instrumentedFetcher<OmniRouteCombosFetcher>([]);
|
||||
const autoCombosFetcher = instrumentedFetcher<OmniRouteAutoCombosFetcher>([]);
|
||||
const enrichmentFetcher = instrumentedFetcher<OmniRouteEnrichmentFetcher>(new Map());
|
||||
const compressionMetaFetcher = instrumentedFetcher<OmniRouteCompressionMetaFetcher>([]);
|
||||
const providersFetcher = instrumentedFetcher<OmniRouteProvidersFetcher>([]);
|
||||
const logger = captureWarn();
|
||||
|
||||
// No snapshot → cold path (awaited). All fetchers must still start
|
||||
// concurrently.
|
||||
const diskSnapshotReader: OmniRouteDiskSnapshotReader = async () => undefined;
|
||||
const diskSnapshotWriter: OmniRouteDiskSnapshotWriter = async () => {};
|
||||
|
||||
const hook = createOmniRouteConfigHook(
|
||||
{ providerId: "omniroute", features: { enrichment: true, compressionMetadata: true, usableOnly: true } },
|
||||
{
|
||||
readAuthJson: authStub(),
|
||||
fetcher,
|
||||
combosFetcher,
|
||||
autoCombosFetcher,
|
||||
enrichmentFetcher,
|
||||
compressionMetaFetcher,
|
||||
providersFetcher,
|
||||
diskSnapshotReader,
|
||||
diskSnapshotWriter,
|
||||
logger,
|
||||
}
|
||||
);
|
||||
|
||||
const input = makeInput();
|
||||
await hook(input);
|
||||
|
||||
// All fetchers should have been called.
|
||||
assert.equal(fetcher.callCount(), 1, "models fetcher called");
|
||||
assert.equal(combosFetcher.callCount(), 1, "combos fetcher called");
|
||||
assert.equal(autoCombosFetcher.callCount(), 1, "autoCombos fetcher called");
|
||||
assert.equal(enrichmentFetcher.callCount(), 1, "enrichment fetcher called");
|
||||
assert.equal(compressionMetaFetcher.callCount(), 1, "compressionMeta fetcher called");
|
||||
assert.equal(providersFetcher.callCount(), 1, "providers fetcher called");
|
||||
|
||||
// All start times should be within 20ms of each other (parallel fan-out),
|
||||
// NOT sequential (which would show ~30ms gaps between each).
|
||||
assert.ok(startTimes.length >= 6, "all 6 fetchers started");
|
||||
const minStart = Math.min(...startTimes);
|
||||
const maxStart = Math.max(...startTimes);
|
||||
assert.ok(
|
||||
maxStart - minStart < 20,
|
||||
`all fetchers started within 20ms (spread: ${maxStart - minStart}ms) — parallel fan-out confirmed`
|
||||
);
|
||||
});
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// (f) Soft-fail parity under Promise.allSettled: per-endpoint fallbacks +
|
||||
// logger.warn breadcrumbs preserved
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
test("warm-startup: combos reject → models-only catalog with warn", async () => {
|
||||
const fetcher = immediateFetcher<OmniRouteModelsFetcher>([MODEL_CLAUDE]);
|
||||
const combosFetcher = throwingFetcher<OmniRouteCombosFetcher>("403 Forbidden");
|
||||
const logger = captureWarn();
|
||||
|
||||
const diskSnapshotReader: OmniRouteDiskSnapshotReader = async () => undefined;
|
||||
const diskSnapshotWriter: OmniRouteDiskSnapshotWriter = async () => {};
|
||||
|
||||
const hook = createOmniRouteConfigHook(
|
||||
{ providerId: "omniroute" },
|
||||
{
|
||||
readAuthJson: authStub(),
|
||||
fetcher,
|
||||
combosFetcher,
|
||||
diskSnapshotReader,
|
||||
diskSnapshotWriter,
|
||||
logger,
|
||||
}
|
||||
);
|
||||
|
||||
const input = makeInput();
|
||||
await hook(input);
|
||||
|
||||
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider[
|
||||
"opencode-omniroute"
|
||||
];
|
||||
assert.ok(entry, "provider entry published");
|
||||
assert.ok(
|
||||
entry.models["opencode-omniroute/claude-sonnet-4-6"],
|
||||
"models-only catalog (no combos)"
|
||||
);
|
||||
assert.ok(
|
||||
logger.entries.some((e) => String(e[0]).includes("/api/combos fetch failed")),
|
||||
"combos-fetch breadcrumb emitted"
|
||||
);
|
||||
});
|
||||
|
||||
test("warm-startup: enrichment rejects → raw-id catalog with warn", async () => {
|
||||
const fetcher = immediateFetcher<OmniRouteModelsFetcher>([MODEL_CLAUDE]);
|
||||
const combosFetcher = immediateFetcher<OmniRouteCombosFetcher>([]);
|
||||
const enrichmentFetcher = throwingFetcher<OmniRouteEnrichmentFetcher>("ETIMEDOUT");
|
||||
const logger = captureWarn();
|
||||
|
||||
const diskSnapshotReader: OmniRouteDiskSnapshotReader = async () => undefined;
|
||||
const diskSnapshotWriter: OmniRouteDiskSnapshotWriter = async () => {};
|
||||
|
||||
const hook = createOmniRouteConfigHook(
|
||||
{ providerId: "omniroute" },
|
||||
{
|
||||
readAuthJson: authStub(),
|
||||
fetcher,
|
||||
combosFetcher,
|
||||
enrichmentFetcher,
|
||||
diskSnapshotReader,
|
||||
diskSnapshotWriter,
|
||||
logger,
|
||||
}
|
||||
);
|
||||
|
||||
const input = makeInput();
|
||||
await hook(input);
|
||||
|
||||
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider[
|
||||
"opencode-omniroute"
|
||||
];
|
||||
assert.ok(entry, "provider entry published");
|
||||
assert.equal(
|
||||
entry.models["opencode-omniroute/claude-sonnet-4-6"].name,
|
||||
"claude-sonnet-4-6",
|
||||
"raw id retained (no enrichment)"
|
||||
);
|
||||
assert.ok(
|
||||
logger.entries.some((e) => String(e[0]).includes("/api/pricing/models fetch failed")),
|
||||
"enrichment-fetch breadcrumb emitted"
|
||||
);
|
||||
});
|
||||
|
||||
test("warm-startup: providers reject → usableOnly filter disabled with warn", async () => {
|
||||
const fetcher = immediateFetcher<OmniRouteModelsFetcher>([MODEL_CLAUDE]);
|
||||
const combosFetcher = immediateFetcher<OmniRouteCombosFetcher>([]);
|
||||
const providersFetcher = throwingFetcher<OmniRouteProvidersFetcher>("ETIMEDOUT");
|
||||
const logger = captureWarn();
|
||||
|
||||
const diskSnapshotReader: OmniRouteDiskSnapshotReader = async () => undefined;
|
||||
const diskSnapshotWriter: OmniRouteDiskSnapshotWriter = async () => {};
|
||||
|
||||
const hook = createOmniRouteConfigHook(
|
||||
{ providerId: "omniroute", features: { usableOnly: true } },
|
||||
{
|
||||
readAuthJson: authStub(),
|
||||
fetcher,
|
||||
combosFetcher,
|
||||
providersFetcher,
|
||||
diskSnapshotReader,
|
||||
diskSnapshotWriter,
|
||||
logger,
|
||||
}
|
||||
);
|
||||
|
||||
const input = makeInput();
|
||||
await hook(input);
|
||||
|
||||
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider[
|
||||
"opencode-omniroute"
|
||||
];
|
||||
assert.ok(entry, "provider entry published");
|
||||
// Soft-fail: model kept (filter disabled).
|
||||
assert.ok(
|
||||
entry.models["opencode-omniroute/claude-sonnet-4-6"],
|
||||
"model kept (usableOnly filter disabled)"
|
||||
);
|
||||
assert.ok(
|
||||
logger.entries.some((e) => String(e[0]).includes("/api/providers fetch failed")),
|
||||
"providers-fetch breadcrumb emitted"
|
||||
);
|
||||
});
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// (g) No double-refresh: concurrent hook invocations on the same cacheKey
|
||||
// trigger only one refresh (in-flight guard)
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
test("warm-startup: concurrent hook invocations dedupe refresh", async () => {
|
||||
let fetchCount = 0;
|
||||
const slowResolve = new Promise<void>((r) => {
|
||||
setTimeout(r, 100);
|
||||
});
|
||||
|
||||
const fetcher: OmniRouteModelsFetcher = async () => {
|
||||
fetchCount++;
|
||||
await slowResolve;
|
||||
return [MODEL_CLAUDE];
|
||||
};
|
||||
const combosFetcher = immediateFetcher<OmniRouteCombosFetcher>([]);
|
||||
const logger = captureWarn();
|
||||
|
||||
const diskSnapshotReader: OmniRouteDiskSnapshotReader = async () => undefined;
|
||||
const diskSnapshotWriter: OmniRouteDiskSnapshotWriter = async () => {};
|
||||
|
||||
const sharedCache: OmniRouteFetchCache = new Map();
|
||||
|
||||
const hook = createOmniRouteConfigHook(
|
||||
{ providerId: "omniroute", modelCacheTtl: 60_000 },
|
||||
{
|
||||
readAuthJson: authStub(),
|
||||
fetcher,
|
||||
combosFetcher,
|
||||
diskSnapshotReader,
|
||||
diskSnapshotWriter,
|
||||
cache: sharedCache,
|
||||
logger,
|
||||
}
|
||||
);
|
||||
|
||||
// Fire two concurrent hook invocations on the same cache.
|
||||
const inputA = makeInput();
|
||||
const inputB = makeInput();
|
||||
await Promise.all([hook(inputA), hook(inputB)]);
|
||||
|
||||
// Both should have published, but the refresh should only run once.
|
||||
assert.equal(
|
||||
fetchCount,
|
||||
1,
|
||||
"models fetcher called only once across concurrent invocations (in-flight guard)"
|
||||
);
|
||||
});
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// (h) features.diskCache: false disables the warm read entirely
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
test("warm-startup: diskCache=false disables warm read, falls through to awaited fetch", async () => {
|
||||
const fetcher = immediateFetcher<OmniRouteModelsFetcher>([MODEL_CLAUDE]);
|
||||
const combosFetcher = immediateFetcher<OmniRouteCombosFetcher>([]);
|
||||
const logger = captureWarn();
|
||||
|
||||
let readerCalled = false;
|
||||
const diskSnapshotReader: OmniRouteDiskSnapshotReader = async () => {
|
||||
readerCalled = true;
|
||||
return {
|
||||
rawModels: [MODEL_GEMINI],
|
||||
rawCombos: [],
|
||||
rawAutoCombos: [],
|
||||
rawEnrichment: new Map(),
|
||||
rawCompressionCombos: [],
|
||||
rawConnections: [],
|
||||
};
|
||||
};
|
||||
const diskSnapshotWriter: OmniRouteDiskSnapshotWriter = async () => {};
|
||||
|
||||
const hook = createOmniRouteConfigHook(
|
||||
{ providerId: "omniroute", features: { diskCache: false } },
|
||||
{
|
||||
readAuthJson: authStub(),
|
||||
fetcher,
|
||||
combosFetcher,
|
||||
diskSnapshotReader,
|
||||
diskSnapshotWriter,
|
||||
logger,
|
||||
}
|
||||
);
|
||||
|
||||
const input = makeInput();
|
||||
await hook(input);
|
||||
|
||||
assert.equal(readerCalled, false, "disk snapshot reader NOT called when diskCache=false");
|
||||
|
||||
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider[
|
||||
"opencode-omniroute"
|
||||
];
|
||||
assert.ok(entry, "provider entry published from live fetch");
|
||||
assert.ok(
|
||||
entry.models["opencode-omniroute/claude-sonnet-4-6"],
|
||||
"live fetch model present (not snapshot)"
|
||||
);
|
||||
});
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// Warm startup: snapshot age logged
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
test("warm-startup: snapshot age is logged when warm-starting from disk", async () => {
|
||||
const fetcher = immediateFetcher<OmniRouteModelsFetcher>([MODEL_CLAUDE]);
|
||||
const combosFetcher = immediateFetcher<OmniRouteCombosFetcher>([]);
|
||||
const logger = captureWarn();
|
||||
|
||||
const snapshot: Omit<import("../src/index.js").OmniRouteFetchCacheEntry, "expiresAt"> & {
|
||||
writtenAt?: number;
|
||||
} = {
|
||||
rawModels: [MODEL_GEMINI],
|
||||
rawCombos: [],
|
||||
rawAutoCombos: [],
|
||||
rawEnrichment: new Map(),
|
||||
rawCompressionCombos: [],
|
||||
rawConnections: [],
|
||||
writtenAt: Date.now() - 3_600_000, // 1 hour ago
|
||||
};
|
||||
|
||||
const diskSnapshotReader: OmniRouteDiskSnapshotReader = async () => snapshot;
|
||||
const diskSnapshotWriter: OmniRouteDiskSnapshotWriter = async () => {};
|
||||
|
||||
const hook = createOmniRouteConfigHook(
|
||||
{ providerId: "omniroute" },
|
||||
{
|
||||
readAuthJson: authStub(),
|
||||
fetcher,
|
||||
combosFetcher,
|
||||
diskSnapshotReader,
|
||||
diskSnapshotWriter,
|
||||
logger,
|
||||
}
|
||||
);
|
||||
|
||||
const input = makeInput();
|
||||
await hook(input);
|
||||
|
||||
// The log should mention "warm startup from disk snapshot".
|
||||
assert.ok(
|
||||
logger.entries.some((e) =>
|
||||
String(e[0]).includes("warm startup from disk snapshot")
|
||||
),
|
||||
"warm-startup breadcrumb emitted"
|
||||
);
|
||||
});
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// Warm startup: empty snapshot (rawModels.length === 0) is skipped
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
test("warm-startup: empty snapshot (rawModels.length=0) is skipped, falls through to fetch", async () => {
|
||||
const fetcher = immediateFetcher<OmniRouteModelsFetcher>([MODEL_CLAUDE]);
|
||||
const combosFetcher = immediateFetcher<OmniRouteCombosFetcher>([]);
|
||||
const logger = captureWarn();
|
||||
|
||||
const diskSnapshotReader: OmniRouteDiskSnapshotReader = async () => ({
|
||||
rawModels: [],
|
||||
rawCombos: [],
|
||||
rawAutoCombos: [],
|
||||
rawEnrichment: new Map(),
|
||||
rawCompressionCombos: [],
|
||||
rawConnections: [],
|
||||
});
|
||||
const diskSnapshotWriter: OmniRouteDiskSnapshotWriter = async () => {};
|
||||
|
||||
const hook = createOmniRouteConfigHook(
|
||||
{ providerId: "omniroute" },
|
||||
{
|
||||
readAuthJson: authStub(),
|
||||
fetcher,
|
||||
combosFetcher,
|
||||
diskSnapshotReader,
|
||||
diskSnapshotWriter,
|
||||
logger,
|
||||
}
|
||||
);
|
||||
|
||||
const input = makeInput();
|
||||
await hook(input);
|
||||
|
||||
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider[
|
||||
"opencode-omniroute"
|
||||
];
|
||||
assert.ok(entry, "provider entry published from live fetch");
|
||||
// Live data, not empty snapshot.
|
||||
assert.ok(
|
||||
entry.models["opencode-omniroute/claude-sonnet-4-6"],
|
||||
"live fetch model present (empty snapshot skipped)"
|
||||
);
|
||||
assert.equal(fetcher.callCount(), 1, "fetcher was called (awaited cold path)");
|
||||
});
|
||||
8
AI-NOTES.md
Normal file
8
AI-NOTES.md
Normal file
@@ -0,0 +1,8 @@
|
||||
# AI-NOTES (Handoff Log)
|
||||
|
||||
## Changelog
|
||||
|
||||
- **2026-08-08 (Antigravity)**:
|
||||
- Status: DONE (Phase 0 and Phase 1).
|
||||
- Actions: Cleaned up PM2 crash-loops (`litellm-proxy`), updated PM2 ecosystem, ran Claude context-optimization (`apply.sh`), removed `.chroma_db` and logs from git, created `HERMES-OMNI-MASTER-PLAN.md` with full 8-phase execution plan.
|
||||
- Next: Phase 2 (Safe cleanup of bundled skills, PM2 port alignment, NPM audit, Doppler secrets unification).
|
||||
142
CLAUDE.md
142
CLAUDE.md
@@ -1,55 +1,117 @@
|
||||
# CLAUDE.md
|
||||
|
||||
@AGENTS.md
|
||||
OmniRoute — unified AI proxy/router. One endpoint, 290 LLM providers, auto-fallback.
|
||||
|
||||
**All project rules live in [`AGENTS.md`](AGENTS.md)** — the single source of truth for every AI
|
||||
assistant (architecture, conventions, testing, quality gates, git workflow, the 22 Hard Rules,
|
||||
PII learnings). Read it in full; do not re-add project rules here. Everything below applies ONLY
|
||||
to Claude Code — operational refinements of rules already defined in `AGENTS.md`.
|
||||
<!-- CONTEXT BUDGET: this file loads in full on every session and after every compact.
|
||||
Keep it under 200 lines. Anything deeper goes to .claude/rules/ (path-scoped,
|
||||
loads only when Claude touches matching files) or .claude/skills/ (loads on demand).
|
||||
Do not re-inline detail here. -->
|
||||
|
||||
## Worktree isolation — Claude Code specifics
|
||||
## Commands
|
||||
|
||||
The full mandatory worktree protocol (base-branch confirmation, `.claude/worktrees/` canonical
|
||||
path, `cp -al` node_modules, teardown rules) is in `AGENTS.md` → Git Workflow → "Worktree
|
||||
isolation". Claude-Code-specific points:
|
||||
```bash
|
||||
npm install # deps (auto-generates .env from .env.example)
|
||||
npm run dev # dev server → http://localhost:20128
|
||||
npm run build # production build (Next.js 16 standalone)
|
||||
npm run lint # ESLint — 0 errors expected
|
||||
npm run typecheck:core # TS check (must be clean)
|
||||
npm run test:unit # Node native runner (most tests)
|
||||
npm run test:vitest # Vitest (MCP server, autoCombo, cache)
|
||||
npm run test:coverage # coverage gate 60/60/60/60
|
||||
npm run check # lint + test
|
||||
node --import tsx/esm --test tests/unit/your-file.test.ts # single file
|
||||
```
|
||||
|
||||
- Confirm the base branch with the operator via `AskUserQuestion` (Hard Rule #19) unless they
|
||||
already told you.
|
||||
- Prefer the native `EnterWorktree` tool — it already creates worktrees under
|
||||
`.claude/worktrees/` (the canonical path). Create the worktree with the documented `git
|
||||
worktree add` command, then call `EnterWorktree` with its `path`.
|
||||
Full test matrix: `CONTRIBUTING.md`. Deep architecture for non-Claude agents: `AGENTS.md`.
|
||||
|
||||
## Cross-session safety — Claude Code specifics
|
||||
## Layout
|
||||
|
||||
Hard Rules #19/#21/#22 (in `AGENTS.md`) govern parallel sessions. Operational reminders for this
|
||||
harness:
|
||||
Monorepo: `src/` (Next.js 16 app) · `open-sse/` (streaming engine workspace) · `electron/` · `tests/` · `bin/` (CLI).
|
||||
|
||||
- **Replicate the `git stash` ban verbatim in the prompt of every subagent that touches git**
|
||||
(Agent tool / Workflow scripts) — subagents do not inherit this file, and the recorded
|
||||
recurrence of the stash incident came through a subagent.
|
||||
- Before merging or pushing to any PR you did not create _this session_, run `git worktree list`
|
||||
and re-check `gh pr view <N> --json state,headRefOid` (Hard Rule #22b).
|
||||
- End every session with the main checkout on the branch it started on.
|
||||
| Layer | Location | Purpose |
|
||||
| ------------- | ----------------------- | -------------------------------------- |
|
||||
| API Routes | `src/app/api/v1/` | App Router entry points |
|
||||
| Handlers | `open-sse/handlers/` | Request processing |
|
||||
| Executors | `open-sse/executors/` | Provider-specific HTTP dispatch |
|
||||
| Translators | `open-sse/translator/` | OpenAI ↔ Claude ↔ Gemini |
|
||||
| Transformer | `open-sse/transformer/` | Responses API ↔ Chat Completions |
|
||||
| Services | `open-sse/services/` | Combo routing, rate limits, caching |
|
||||
| Database | `src/lib/db/` | SQLite domain modules (130 migrations) |
|
||||
| Domain/Policy | `src/domain/` | Policy engine, cost rules, fallback |
|
||||
| MCP Server | `open-sse/mcp-server/` | 104 tools, 3 transports, 31 scopes |
|
||||
| A2A Server | `src/lib/a2a/` | JSON-RPC 2.0 agent protocol |
|
||||
| Skills | `src/lib/skills/` | Sandbox skill framework |
|
||||
| Memory | `src/lib/memory/` | Persistent conversational memory |
|
||||
|
||||
## Superpowers / planning artifacts — path overrides
|
||||
Request pipeline: `route → CORS → Zod validation → optional auth → API-key policy → injection guard → handleChatCore() → cache → rate limit → combo routing → translate → executor → upstream fetch (retry/backoff) → translate back → SSE or JSON`. No global Next.js middleware; interception is per-route.
|
||||
|
||||
The `_tasks/` convention is defined in `AGENTS.md` → "Planning & Research Artifacts". The
|
||||
superpowers skills ship with defaults that point at `docs/…` — those defaults are **overridden
|
||||
here**. When a superpowers skill announces a path like "saved to `docs/superpowers/plans/…`",
|
||||
rewrite it to the `_tasks/…` equivalent before writing:
|
||||
## Hard rules
|
||||
|
||||
| Artifact (skill) | Default (do NOT use) | Save here instead |
|
||||
| ---------------------------------- | ------------------------- | ------------------------------------------------------------- |
|
||||
| Plans (`writing-plans`) | `docs/superpowers/plans/` | `_tasks/superpowers/plans/YYYY-MM-DD-<feature>.md` |
|
||||
| Specs / design (`brainstorming`) | `docs/superpowers/specs/` | `_tasks/superpowers/specs/YYYY-MM-DD-<topic>-design.md` |
|
||||
| Research (`deep-research`, ad-hoc) | `docs/research/` | `_tasks/research/…` |
|
||||
| Hand-offs (`/handoff`) | — | `_tasks/hands-off/<YYYY-MM-DD>_<branch>_v<versão>_sess-<id>/` |
|
||||
Violating any of these is a blocking defect. Numbers are stable — cite them in review.
|
||||
|
||||
Commit those artifacts inside the `_tasks/` repo (`git -C _tasks …`), never in the main repo.
|
||||
1. Never commit secrets or credentials.
|
||||
2. Never add logic to `src/lib/localDb.ts` (re-export layer only); never barrel-import from it.
|
||||
3. Never use `eval()` / `new Function()` / implied eval.
|
||||
4. Never commit directly to `main`.
|
||||
5. Never write raw SQL in routes or handlers — go through `src/lib/db/` modules.
|
||||
6. Never silently swallow errors in SSE streams.
|
||||
7. Always validate inputs with Zod.
|
||||
8. Always include or update tests in the same PR that changes production code in `src/`, `open-sse/`, `electron/`, `bin/`.
|
||||
9. Coverage must not regress below `quality-baseline.json`; absolute floor 60/60/60/60.
|
||||
10. Never bypass Husky hooks (`--no-verify`, `--no-gpg-sign`) without explicit operator approval.
|
||||
11. Never embed public upstream OAuth client_id/secret or Firebase Web keys as literals — use `resolvePublicCred()`.
|
||||
12. Never return raw `err.stack` / `err.message` in HTTP / SSE / executor / MCP responses — use `buildErrorBody()` or `sanitizeErrorMessage()`.
|
||||
13. Never string-interpolate external paths into shell scripts passed to `exec()`/`spawn()` — pass via the `env` option.
|
||||
14. Never dismiss a CodeQL / secret-scanning alert without checking the sanitizer docs first and recording the justification.
|
||||
15. Never expose child-process-spawning routes (`/api/mcp/`, `/api/cli-tools/runtime/`) without `isLocalOnlyPath()` classification.
|
||||
16. Never credit an AI assistant in commit/PR metadata — no `Co-Authored-By` naming an AI/bot, no "Generated with …" footers, anywhere. This overrides any harness default; strip it before pushing. Human collaborators are still credited normally.
|
||||
17. Never expose `/api/services/` or `/dashboard/providers/services/*/embed/` without `isLocalOnlyPath()` classification.
|
||||
18. Every bug fix ships with a failing-then-passing test (TDD, preferred) **or** a documented live test on the production VPS `192.168.0.15`. "Worked locally" is not validation.
|
||||
19. Never develop on the shared main checkout. Every task gets its own worktree under `.claude/worktrees/`, on its own branch, cut from a base branch the operator confirmed. → skill `omniroute-worktree`
|
||||
20. PII redaction/sanitization is opt-in. `PII_REDACTION_ENABLED` and `PII_RESPONSE_SANITIZATION` must keep `defaultValue: "false"`.
|
||||
21. During an active `release-freeze`, never merge into the frozen `release/vX.Y.Z`; retarget to the highest `release/v*` and verify the retarget. Only `/generate-release` may raise or lift a freeze. → skill `omniroute-release-freeze`
|
||||
22. Cross-session safety — many parallel sessions work this repo:
|
||||
- **Never `git stash` / `git stash pop`, anywhere in this repo, including inside a worktree and inside any subagent you dispatch.** The stash lives in the shared object store and clobbers other sessions' uncommitted work. Compare against a base with `git show <ref>:<path>` or `git diff <ref> -- <path>` instead. Repeat this ban verbatim in the prompt of every subagent that touches git.
|
||||
- Never merge, push, rebase, or force-push a PR / branch / worktree another session owns. Check `git worktree list` and `gh pr view <N> --json state,headRefOid` before touching a PR you did not open this session.
|
||||
|
||||
## Base-green before opening PRs
|
||||
## Conventions (always-on subset)
|
||||
|
||||
Before cutting a branch or opening a PR, run the base-green check (`AGENTS.md` → Git Workflow →
|
||||
"Base-green check"; project skills reference it as `.agents/skills/_shared/base-green.md`). A PR
|
||||
opened while the base tip is red must carry `⚠️ base-red inherited: #<issue>` in its body. To
|
||||
drain an accumulated red state (base tip + red PRs), use the `/sweep-reds` skill.
|
||||
- 2 spaces, semicolons, double quotes, 100 cols, es5 trailing commas (Prettier via lint-staged).
|
||||
- Imports: external → internal (`@/`, `@omniroute/open-sse`) → relative.
|
||||
- Naming: files camelCase/kebab, components PascalCase, constants UPPER_SNAKE.
|
||||
- `no-explicit-any` is an **error** in `open-sse/` and `tests/`; pre-existing violations are frozen in `config/quality/eslint-suppressions.json`.
|
||||
- TypeScript `strict: false`, target ES2022, module esnext, resolution bundler. Prefer explicit types.
|
||||
- Commits: Conventional Commits, `feat(db): …`. Branch prefixes `feat/ fix/ refactor/ docs/ test/ chore/`.
|
||||
|
||||
## Environment
|
||||
|
||||
Node ≥22 <23 || ≥24 <27, ESM — the only supported runtime. Bun 1.3.14 is pinned as an exact devDependency for an allow-listed set of gate/generator scripts and the `test:bun:db` smoke only; do not widen it to install, build, or the main test runners. Port 20128. `DATA_DIR` defaults to `~/.omniroute/`. Aliases: `@/*` → `src/`, `@omniroute/open-sse` → `open-sse/`.
|
||||
|
||||
## Planning artifacts
|
||||
|
||||
`_tasks/` is a separate, gitignored git repo and is the only place for plans, specs, research, and hand-offs. Never write superpowers / planning / research output under `docs/` or the repo root, even when a skill's default says `docs/superpowers/…` — rewrite the path to `_tasks/…` before writing, and commit inside `_tasks/` (`git -C _tasks …`).
|
||||
|
||||
## Where the rest lives
|
||||
|
||||
Do not ask for these upfront — they load themselves when relevant.
|
||||
|
||||
**Path-scoped rules** (`.claude/rules/`, auto-load when you open matching files): `database.md`, `api-routes.md`, `open-sse.md`, `resilience.md`, `security-sensitive.md`, `pii.md`, `testing.md`.
|
||||
|
||||
**Skills** (`.claude/skills/`, load on demand): `omniroute-worktree`, `omniroute-release-freeze`, `omniroute-extend` (add a provider / route / DB module / MCP tool / A2A skill / cloud agent / embedded service), `omniroute-quality-gates`.
|
||||
|
||||
**Docs** — read the matching deep-dive before any non-trivial change:
|
||||
|
||||
| Area | Doc |
|
||||
| ---------------------------------------------- | ---------------------------------------------------------------------------- |
|
||||
| Repo navigation | `docs/architecture/REPOSITORY_MAP.md` |
|
||||
| Architecture / engineering reference | `docs/architecture/ARCHITECTURE.md`, `CODEBASE_DOCUMENTATION.md` |
|
||||
| Auto-Combo (13-factor, 19 strategies) | `docs/routing/AUTO-COMBO.md` |
|
||||
| Resilience (3 mechanisms) | `docs/architecture/RESILIENCE_GUIDE.md` |
|
||||
| Reasoning replay | `docs/routing/REASONING_REPLAY.md` |
|
||||
| Skills / Memory / Cloud agents | `docs/frameworks/SKILLS.md`, `MEMORY.md`, `CLOUD_AGENT.md` |
|
||||
| Guardrails / public creds / error sanitization | `docs/security/GUARDRAILS.md`, `PUBLIC_CREDS.md`, `ERROR_SANITIZATION.md` |
|
||||
| Authorization / route guard tiers | `docs/architecture/AUTHZ_GUIDE.md`, `docs/security/ROUTE_GUARD_TIERS.md` |
|
||||
| Agent protocols / MCP / A2A | `docs/frameworks/AGENT_PROTOCOLS_GUIDE.md`, `MCP-SERVER.md`, `A2A-SERVER.md` |
|
||||
| API reference | `docs/reference/API_REFERENCE.md` + `docs/openapi.yaml` |
|
||||
| Release flow / embedded services | `docs/ops/RELEASE_CHECKLIST.md`, `docs/frameworks/EMBEDDED-SERVICES.md` |
|
||||
| Quality gates (~48 scripts) | `docs/architecture/QUALITY_GATES.md` |
|
||||
|
||||
14
OS-NOTES.md
Normal file
14
OS-NOTES.md
Normal file
@@ -0,0 +1,14 @@
|
||||
# OS-NOTES (Dashboard & Roadmap)
|
||||
|
||||
## Dashboard
|
||||
|
||||
- OmniRoute / Hermes integration active.
|
||||
- Context optimization script applied.
|
||||
- Master plan drafted (`HERMES-OMNI-MASTER-PLAN.md`).
|
||||
|
||||
## Roadmap
|
||||
|
||||
- [done] Phase 0 & 1: Inventory and context optimization.
|
||||
- [next] Phase 2: Secrets and stability (resolve Doppler configs `prd` vs `dev_personal`, fix `omniroute-tunnel`, safe skill symlink cleanup).
|
||||
- [next] Phase 3: AGY / Claude Code integration via OmniRoute.
|
||||
- [next] Phase 4: Combo evaluations.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
feature: 9490
|
||||
---
|
||||
|
||||
**Warm catalog startup from disk snapshot + parallel refresh** (opencode-plugin): The config-shim hook now reads the last disk snapshot *before* fetching, so the provider registers immediately with the last-known-good catalog (~1-2s vs ~30s on a warm gateway). All six fetchers run concurrently via `Promise.allSettled` instead of sequentially. A failed refresh keeps the snapshot (no overwrite). An in-flight guard prevents concurrent refreshes for the same cache key. The `features.diskCache: false` opt-out disables the warm read entirely.
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"_rebaseline_2026_08_09_9296_adobe_media_capabilities": "PR #9296 (artickc, fix/adobe-firefly-model-capabilities) own growth: src/app/api/v1/models/catalog.ts 1590->1597 (+7). The image and video catalog serializers now expose the already-normalized Adobe Firefly discovery capability data (media_capabilities, plus the existing video modality/size fields) at their only response-emission chokepoints. The discovery parser and capability normalization remain in open-sse/services/adobeFireflyModels.ts; extracting these seven serialization fields would obscure the catalog contract. Covered by tests/unit/adobe-firefly.test.ts and tests/unit/image-upscale.test.ts.",
|
||||
"_rebaseline_2026_07_24_8470_hyperagent_sticky_thread": "PR #8470 (artickc, fix/hyperagent-tool-loop-thread-sticky) own growth: open-sse/executors/hyperagent.ts 936->1025 (wc -l; check-file-size.mjs counts via split(\"\\n\").length so the gate sees 937->1026, +89, crosses the 1000 cap). Fixes a real bug where a reverse-conversion proxy (text-Intent/JSON to Claude Code native tool_calls) rewrites assistant messages between agentic tool-loop turns, breaking HyperAgent’s conversation-prefix fingerprint and cold-starting the thread mid tool-loop. Adds Anthropic tool_use/tool_result flattening to extractMessageText() plus a new rootUserFingerprint()/root-key lookup tier in resolveHyperAgentThreadBinding()/storeHyperAgentThreadAfterTurn() so the thread stays sticky across the tool loop. Cohesive additions inside the existing single-file executor; not extractable without splitting the executor mid-request-flow. Covered by tests/unit/executor-hyperagent.test.ts (19/19, +5 new cases for tool_result/tool_use flattening + root-key stickiness). Pre-merge review flagged a cross-conversation root-key collision risk (tracked in the PR’s own mandatory pre-merge checklist, not yet addressed) — unrelated to this file-size ratchet, tracked separately by /fix-prs.",
|
||||
"_rebaseline_2026_07_25_8494_capability_filter_fail_closed": "PR #8494 (fix/capability-filters-fail-closed, #8488) own growth: open-sse/services/combo.ts 3640->3693 (+53) adds a fail-closed guard after filterTargetsByRequestCompatibility() — when every eligible target is excluded by request-capability filtering (vision/tools/etc) instead of quota/health, the combo now returns an explicit `capability_mismatch` 400 (describeCapabilityFilterExhaustion, imported from combo/comboStructure.ts) rather than silently falling through to a generic no-targets error, plus a `compatFilterFailOpen` escape hatch (combo config OR settings) mirrored at both the main/auto and round-robin call sites for symmetry. combo/comboStructure.ts (previously under cap, un-frozen) grows 794->918 (+124) — new home for describeCapabilityFilterExhaustion + providerSupportsEmulatedToolCalling (#5240 emulated tool-calling exemption so fail-closed does not regress prompt-emulation-only combos like all-chatgpt-web). Irreducible orchestration wiring at the existing filter chokepoint (same precedent as #7301's universal-cooldown-retry generalization). Companion test tests/unit/combo-routing-engine.test.ts 3409->3449 (+40, fail-closed/fail-open coverage across both call sites) also rebaselined. Covered by tests/unit/8488-capability-filter-fail-closed.test.ts (new) + 95/95 passing across both files. Structural shrink of combo.ts tracked in #3501.",
|
||||
"_rebaseline_2026_07_25_8499_ts7_result_union_predicates": "PR #8499 (backryun, chore/ts7-types-executor-scattered) own growth: muse-spark-web.ts 1396->1405 (+9, irreducible). Under this workspace's `strictNullChecks: false`, the boolean-literal discriminant on `GraphqlResult` (`{ ok: true } | { ok: false; error: string }`) narrows the positive `.ok===true` branch but leaves `!result.ok` at the full union under TS7, making `.error` unreachable to the checker at the two call sites (warmup, mode-switch). Fixed by adding a single `isGraphqlFailure()` type-predicate helper (doc comment + 3-line body) reused at both call sites instead of duplicating the predicate inline — not extractable to a shared module without splitting a single-file executor's local narrowing helper out of its own file. Covered by the existing muse-spark-web executor test suite (no behavior change, pure narrowing fix).",
|
||||
@@ -387,7 +388,7 @@
|
||||
"src/app/(dashboard)/dashboard/usage/components/EvalsTab.tsx": 2148,
|
||||
"src/app/(dashboard)/dashboard/usage/components/ProviderLimits/index.tsx": 1119,
|
||||
"src/app/api/providers/[id]/models/route.ts": 2361,
|
||||
"src/app/api/v1/models/catalog.ts": 1590,
|
||||
"src/app/api/v1/models/catalog.ts": 1597,
|
||||
"src/lib/db/apiKeys.ts": 1529,
|
||||
"src/lib/db/core.ts": 1639,
|
||||
"src/lib/db/migrationRunner.ts": 1094,
|
||||
@@ -536,7 +537,7 @@
|
||||
"src/app/(dashboard)/dashboard/usage/components/EvalsTab.tsx": "2148",
|
||||
"src/app/(dashboard)/dashboard/usage/components/ProviderLimits/index.tsx": "1119",
|
||||
"src/app/api/providers/[id]/models/route.ts": "2361",
|
||||
"src/app/api/v1/models/catalog.ts": "1590",
|
||||
"src/app/api/v1/models/catalog.ts": "1597",
|
||||
"src/lib/tokenHealthCheck.ts": "1053",
|
||||
"src/lib/db/apiKeys.ts": "1529",
|
||||
"src/lib/db/core.ts": "1639",
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
---
|
||||
title: "AgentRouter WAF"
|
||||
version: 3.8.50
|
||||
lastUpdated: 2026-08-03
|
||||
---
|
||||
|
||||
# agentrouter.org WAF (Web Application Firewall)
|
||||
@@ -29,22 +27,22 @@ The WAF inspects `messages[].content` only. It does **not** inspect:
|
||||
|
||||
## Always-blocked patterns (case-insensitive)
|
||||
|
||||
| Pattern | Notes |
|
||||
|-------------------------------|----------------------------------------|
|
||||
| Any `Lorem ipsum` variant | Full Latin lorem vocabulary is blocked |
|
||||
| `language model` (alone) | "the language model" and "large language model" pass |
|
||||
| `virtual assistant` | "AI assistant" passes |
|
||||
| `I'm here to help` | "here to help" alone also blocks |
|
||||
| `Claude, made by Anthropic` | Full phrase only |
|
||||
| Pattern | Notes |
|
||||
| --------------------------- | ---------------------------------------------------- |
|
||||
| Any `Lorem ipsum` variant | Full Latin lorem vocabulary is blocked |
|
||||
| `language model` (alone) | "the language model" and "large language model" pass |
|
||||
| `virtual assistant` | "AI assistant" passes |
|
||||
| `I'm here to help` | "here to help" alone also blocks |
|
||||
| `Claude, made by Anthropic` | Full phrase only |
|
||||
|
||||
## Almost-always-blocked patterns
|
||||
|
||||
| Pattern | Notes |
|
||||
|-------------------|---------------------------------------------------------|
|
||||
| `placeholder` | When it stands alone (not as a parameter name, etc.) |
|
||||
| `dummy data` | Common seed phrase for fixtures |
|
||||
| `foo bar baz` | Canonical placeholder phrase |
|
||||
| Repeated short tokens (`AAA BBB CCC`, `test test test`) | Detector for keyword stuffing |
|
||||
| Pattern | Notes |
|
||||
| ------------------------------------------------------- | ---------------------------------------------------- |
|
||||
| `placeholder` | When it stands alone (not as a parameter name, etc.) |
|
||||
| `dummy data` | Common seed phrase for fixtures |
|
||||
| `foo bar baz` | Canonical placeholder phrase |
|
||||
| Repeated short tokens (`AAA BBB CCC`, `test test test`) | Detector for keyword stuffing |
|
||||
|
||||
## Behavior under load
|
||||
|
||||
@@ -94,4 +92,4 @@ The current filter is overly aggressive — it blocks "Lorem ipsum" in
|
||||
`tool_result` blocks even though the operator clearly did not intend to
|
||||
inject a prompt. Operators who want this fixed at the source should
|
||||
contact `agentrouter.org` to report the false positives. The blocklist
|
||||
above is the empirical result of probing the upstream as of 2026-08-03.
|
||||
above is the empirical result of probing the upstream as of 2026-08-03.
|
||||
|
||||
44
ecosystem.config.cjs
Normal file
44
ecosystem.config.cjs
Normal file
@@ -0,0 +1,44 @@
|
||||
module.exports = {
|
||||
apps: [
|
||||
{
|
||||
name: "omniroute",
|
||||
script: "npm",
|
||||
args: "run start",
|
||||
cwd: "/Users/work/hermes-sandbox/Hermes_Omni",
|
||||
autorestart: true,
|
||||
watch: false,
|
||||
min_uptime: "10s",
|
||||
max_restarts: 10,
|
||||
exp_backoff_restart_delay: 100,
|
||||
restart_delay: 5000,
|
||||
max_memory_restart: "700M",
|
||||
kill_timeout: 10000,
|
||||
listen_timeout: 10000,
|
||||
time: true,
|
||||
merge_logs: true,
|
||||
env: {
|
||||
NODE_ENV: "production",
|
||||
PORT: 20128
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
name: "hermes-tg-bot",
|
||||
script: "/Users/work/hermes-sandbox/Hermes_Omni/scripts/serpentos_logic/tg-bot.cjs",
|
||||
cwd: "/Users/work/hermes-sandbox/Hermes_Omni",
|
||||
autorestart: true,
|
||||
watch: false,
|
||||
min_uptime: "10s",
|
||||
max_restarts: 10,
|
||||
exp_backoff_restart_delay: 500,
|
||||
restart_delay: 5000,
|
||||
max_memory_restart: "500M",
|
||||
kill_timeout: 10000,
|
||||
time: true,
|
||||
merge_logs: true,
|
||||
env: {
|
||||
NODE_ENV: "production"
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
22
insert_mappings.cjs
Normal file
22
insert_mappings.cjs
Normal file
@@ -0,0 +1,22 @@
|
||||
const Database = require('better-sqlite3');
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
|
||||
const db = new Database('/Users/work/.omniroute/storage.sqlite');
|
||||
const combos = db.prepare("SELECT id, name FROM combos").all();
|
||||
const now = new Date().toISOString();
|
||||
|
||||
for (const combo of combos) {
|
||||
const mappingId = uuidv4();
|
||||
// Map the combo name directly as the pattern
|
||||
try {
|
||||
db.prepare(`
|
||||
INSERT INTO model_combo_mappings
|
||||
(id, pattern, combo_id, priority, enabled, description, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(mappingId, combo.name, combo.id, 100, 1, `Auto-mapped combo: ${combo.name}`, now, now);
|
||||
console.log(`Mapped pattern '${combo.name}' to combo '${combo.name}'`);
|
||||
} catch(e) {
|
||||
console.log(`Error mapping ${combo.name}: ${e.message}`);
|
||||
}
|
||||
}
|
||||
console.log("Done.");
|
||||
24
insert_mappings.js
Normal file
24
insert_mappings.js
Normal file
@@ -0,0 +1,24 @@
|
||||
const Database = require("better-sqlite3");
|
||||
const { v4: uuidv4 } = require("uuid");
|
||||
|
||||
const db = new Database("/Users/work/.omniroute/storage.sqlite");
|
||||
const combos = db.prepare("SELECT id, name FROM combos").all();
|
||||
const now = new Date().toISOString();
|
||||
|
||||
for (const combo of combos) {
|
||||
const mappingId = uuidv4();
|
||||
// Map the combo name directly as the pattern
|
||||
try {
|
||||
db.prepare(
|
||||
`
|
||||
INSERT INTO model_combo_mappings
|
||||
(id, pattern, combo_id, priority, enabled, description, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`
|
||||
).run(mappingId, combo.name, combo.id, 100, 1, `Auto-mapped combo: ${combo.name}`, now, now);
|
||||
console.log(`Mapped pattern '${combo.name}' to combo '${combo.name}'`);
|
||||
} catch (e) {
|
||||
console.log(`Error mapping ${combo.name}: ${e.message}`);
|
||||
}
|
||||
}
|
||||
console.log("Done.");
|
||||
@@ -12,6 +12,10 @@ import { FREEPIK_IMAGE_PROVIDER } from "./providers/registry/freepik/index.ts";
|
||||
import { STABILITY_AI_IMAGE_MODELS } from "./providers/registry/stability-ai/imageModels.ts";
|
||||
import { GEMINI_IMAGEN_PROVIDER } from "./providers/registry/gemini/imageModels.ts";
|
||||
import { CHEAPERINFERENCE_IMAGE_PROVIDER } from "./providers/registry/cheaperinference/imageModels.ts";
|
||||
import {
|
||||
ADOBE_FIREFLY_IMAGE_ROUTING_ALIASES,
|
||||
toRegistryImageModels,
|
||||
} from "../services/adobeFireflyModels.ts";
|
||||
|
||||
interface ImageModelEntry {
|
||||
id: string;
|
||||
@@ -22,6 +26,8 @@ interface ImageModelEntry {
|
||||
imageRequired?: boolean;
|
||||
description?: string;
|
||||
isMarket?: boolean;
|
||||
supportedSizes?: string[];
|
||||
mediaCapabilities?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
interface ImageProviderConfig {
|
||||
@@ -35,6 +41,7 @@ interface ImageProviderConfig {
|
||||
authHeader: string;
|
||||
format: string;
|
||||
models: ImageModelEntry[];
|
||||
routingAliases?: readonly string[];
|
||||
supportedSizes: string[];
|
||||
}
|
||||
|
||||
@@ -46,6 +53,7 @@ interface ImageModelAliasEntry {
|
||||
inputModalities?: string[];
|
||||
imageRequired?: boolean;
|
||||
description?: string;
|
||||
mediaCapabilities?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
interface ImageCatalogModelEntry {
|
||||
@@ -55,6 +63,7 @@ interface ImageCatalogModelEntry {
|
||||
supportedSizes: string[];
|
||||
inputModalities: string[];
|
||||
description?: string;
|
||||
mediaCapabilities?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
const IMAGE_MODEL_ALIASES: Record<string, ImageModelAliasEntry> = {
|
||||
@@ -678,55 +687,9 @@ export const IMAGE_PROVIDERS: Record<string, ImageProviderConfig> = {
|
||||
authType: "apikey",
|
||||
authHeader: "bearer",
|
||||
format: "adobe-firefly-image",
|
||||
models: [
|
||||
{
|
||||
id: "nano-banana-pro",
|
||||
name: "Firefly Gemini 3.0 (Nano Banana Pro)",
|
||||
inputModalities: ["text", "image"],
|
||||
},
|
||||
{
|
||||
id: "nano-banana",
|
||||
name: "Firefly Gemini 2.5 (Nano Banana)",
|
||||
inputModalities: ["text", "image"],
|
||||
},
|
||||
{
|
||||
id: "nano-banana-2",
|
||||
name: "Firefly Gemini 3.1 (Nano Banana 2)",
|
||||
inputModalities: ["text", "image"],
|
||||
},
|
||||
{ id: "gpt-image-2", name: "Firefly GPT Image 2", inputModalities: ["text", "image"] },
|
||||
{ id: "gpt-image", name: "Firefly GPT Image 2", inputModalities: ["text", "image"] },
|
||||
{ id: "gpt-image-1.5", name: "Firefly GPT Image 1.5", inputModalities: ["text", "image"] },
|
||||
{ id: "flux-2", name: "Firefly Flux 2", inputModalities: ["text", "image"] },
|
||||
{ id: "flux-pro", name: "Firefly Flux 1.1 Pro", inputModalities: ["text", "image"] },
|
||||
{ id: "flux-ultra", name: "Firefly Flux 1.1 Ultra", inputModalities: ["text", "image"] },
|
||||
{ id: "seedream-4", name: "Firefly Seedream 4.0", inputModalities: ["text", "image"] },
|
||||
{
|
||||
id: "seedream-5-lite",
|
||||
name: "Firefly Seedream 5.0 Lite",
|
||||
inputModalities: ["text", "image"],
|
||||
},
|
||||
{
|
||||
id: "runway-gen4-image",
|
||||
name: "Firefly Runway Gen-4 Image",
|
||||
inputModalities: ["text", "image"],
|
||||
},
|
||||
// Topaz Labs upscalers (inputMediaUseCase: ["upscaling"]).
|
||||
// Served by firefly-3p /v2/3p-images/upsample — see config/upscaleRegistry.ts.
|
||||
{
|
||||
id: "topaz-standard",
|
||||
name: "Firefly Topaz Upscale (Standard)",
|
||||
inputModalities: ["image"],
|
||||
imageRequired: true,
|
||||
},
|
||||
{
|
||||
id: "topaz-bloom",
|
||||
name: "Firefly Topaz Bloom (Creative Upscale)",
|
||||
inputModalities: ["image"],
|
||||
imageRequired: true,
|
||||
},
|
||||
],
|
||||
supportedSizes: ["1:1", "16:9", "9:16", "4:3", "3:4", "1024x1024", "1792x1024", "1024x1792"],
|
||||
models: toRegistryImageModels(),
|
||||
routingAliases: ADOBE_FIREFLY_IMAGE_ROUTING_ALIASES,
|
||||
supportedSizes: [],
|
||||
},
|
||||
|
||||
// Cheaper Inference (OSS-sponsor gateway). Declared AFTER adobe-firefly on
|
||||
@@ -887,7 +850,7 @@ export function parseImageModel(modelStr) {
|
||||
|
||||
// No provider prefix — try to find the model in every provider
|
||||
for (const [providerId, config] of Object.entries(IMAGE_PROVIDERS)) {
|
||||
if (config.models.some((m) => m.id === modelStr)) {
|
||||
if (config.routingAliases?.includes(modelStr) || config.models.some((m) => m.id === modelStr)) {
|
||||
return { provider: providerId, model: modelStr };
|
||||
}
|
||||
}
|
||||
@@ -906,9 +869,10 @@ function imageProviderCatalogEntries(
|
||||
id: `${providerId}/${model.id}`,
|
||||
name: model.name,
|
||||
provider: providerId,
|
||||
supportedSizes: config.supportedSizes,
|
||||
supportedSizes: model.supportedSizes || config.supportedSizes,
|
||||
inputModalities: model.inputModalities || ["text"],
|
||||
description: model.description || undefined,
|
||||
mediaCapabilities: model.mediaCapabilities,
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
@@ -5,14 +5,17 @@
|
||||
* Supports local providers plus hosted task-based APIs such as Runway.
|
||||
*/
|
||||
|
||||
import { parseModelFromRegistry, getAllModelsFromRegistry } from "./registryUtils.ts";
|
||||
import { parseModelFromRegistry } from "./registryUtils.ts";
|
||||
import { RUNWAYML_SUPPORTED_VIDEO_MODELS } from "./runway.ts";
|
||||
import { SEGMIND_VIDEO_MODELS } from "./providers/registry/segmind/videoModels.ts";
|
||||
import { toRegistryVideoModels } from "../services/adobeFireflyModels.ts";
|
||||
|
||||
interface VideoModel {
|
||||
id: string;
|
||||
name: string;
|
||||
isMarket?: boolean;
|
||||
supportedSizes?: string[];
|
||||
mediaCapabilities?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
interface VideoProvider {
|
||||
@@ -326,8 +329,7 @@ export const VIDEO_PROVIDERS: Record<string, VideoProvider> = {
|
||||
},
|
||||
|
||||
// Adobe Firefly (unofficial) — same IMS/cookie credential as the image entry.
|
||||
// Async 3P video generate + poll (Sora 2, Veo 3.1, Kling …). Fallback list
|
||||
// from models/discovery capture (adobe/get_models.txt).
|
||||
// Exact async video models and capabilities from the verified discovery snapshot.
|
||||
"adobe-firefly": {
|
||||
id: "adobe-firefly",
|
||||
alias: "firefly",
|
||||
@@ -335,18 +337,7 @@ export const VIDEO_PROVIDERS: Record<string, VideoProvider> = {
|
||||
authType: "apikey",
|
||||
authHeader: "bearer",
|
||||
format: "adobe-firefly-video",
|
||||
models: [
|
||||
{ id: "sora-2", name: "Firefly Sora 2" },
|
||||
{ id: "sora-2-pro", name: "Firefly Sora 2 Pro" },
|
||||
{ id: "veo-3.1", name: "Firefly Veo 3.1" },
|
||||
{ id: "veo-3.1-fast", name: "Firefly Veo 3.1 Fast" },
|
||||
{ id: "veo-3.1-ref", name: "Firefly Veo 3.1 Reference" },
|
||||
{ id: "kling-3", name: "Firefly Kling v3 Standard I2V" },
|
||||
{ id: "kling-v3-t2v", name: "Firefly Kling v3 Standard T2V" },
|
||||
{ id: "kling-v3-pro-i2v", name: "Firefly Kling v3 Pro I2V" },
|
||||
{ id: "luma-ray3", name: "Firefly Ray3" },
|
||||
{ id: "runway-gen4-turbo", name: "Firefly Runway Gen-4 Video" },
|
||||
],
|
||||
models: toRegistryVideoModels(),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -368,5 +359,17 @@ export function parseVideoModel(modelStr: string | null) {
|
||||
* Get all video models as a flat list
|
||||
*/
|
||||
export function getAllVideoModels() {
|
||||
return getAllModelsFromRegistry(VIDEO_PROVIDERS);
|
||||
return Object.entries(VIDEO_PROVIDERS).flatMap(([providerId, config]) =>
|
||||
[providerId, config.alias]
|
||||
.filter((prefix): prefix is string => Boolean(prefix))
|
||||
.flatMap((prefix) =>
|
||||
config.models.map((model) => ({
|
||||
id: `${prefix}/${model.id}`,
|
||||
name: model.name,
|
||||
provider: providerId,
|
||||
supportedSizes: model.supportedSizes || [],
|
||||
mediaCapabilities: model.mediaCapabilities,
|
||||
}))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -16,11 +16,11 @@ import {
|
||||
AdobeFireflyError,
|
||||
adobeFireflyGenerateImage,
|
||||
adobeFireflyImageTimeoutMs,
|
||||
adobeFireflyMaxImageRefs,
|
||||
resolveAdobeAccessToken,
|
||||
resolveAdobeSourceImageIds,
|
||||
resolveAdobeSourceImageReferences,
|
||||
resolveAdobeImageModel,
|
||||
} from "../../../services/adobeFireflyClient.ts";
|
||||
import { getAdobeReferenceUploadLimit } from "../../../services/adobeFireflyModels.ts";
|
||||
import { isAdobeFireflyUpscaleModel } from "../../../services/adobeFireflyUpscale.ts";
|
||||
import { handleAdobeFireflyImageUpscale } from "../../imageUpscale/adobeFirefly.ts";
|
||||
|
||||
@@ -90,7 +90,8 @@ export async function handleAdobeFireflyImageGeneration({
|
||||
|
||||
// Keep the raw credential blob for Cookie + sherlockToken (x-arp-session-id).
|
||||
// JWT may be embedded in the same paste as cookies (HAR / multi-line).
|
||||
const psd = (credentials as { providerSpecificData?: { cookie?: string } })?.providerSpecificData;
|
||||
const psd = (credentials as { providerSpecificData?: { cookie?: string } })
|
||||
?.providerSpecificData;
|
||||
const sessionCookie =
|
||||
(typeof psd?.cookie === "string" && psd.cookie.trim()) ||
|
||||
(typeof credentials?.apiKey === "string" && credentials.apiKey.trim()) ||
|
||||
@@ -98,15 +99,11 @@ export async function handleAdobeFireflyImageGeneration({
|
||||
? credentials.accessToken
|
||||
: undefined);
|
||||
|
||||
// Cap uploads by model family. gpt-image: 2 subject refs max (3–4+ stalls colligo → 504).
|
||||
// nano: 4 general refs for multi-panel composition.
|
||||
const { id: resolvedId } = resolveAdobeImageModel(model);
|
||||
const maxRefs = adobeFireflyMaxImageRefs(resolvedId);
|
||||
|
||||
const sourceImageIds = await resolveAdobeSourceImageIds({
|
||||
const { spec } = resolveAdobeImageModel(model);
|
||||
const references = await resolveAdobeSourceImageReferences({
|
||||
accessToken,
|
||||
body,
|
||||
max: maxRefs,
|
||||
max: getAdobeReferenceUploadLimit(spec, "image"),
|
||||
sessionCookie,
|
||||
prompt,
|
||||
fetchImpl,
|
||||
@@ -121,13 +118,13 @@ export async function handleAdobeFireflyImageGeneration({
|
||||
: undefined;
|
||||
const timeoutMs = adobeFireflyImageTimeoutMs({
|
||||
timeoutMs: explicitTimeout,
|
||||
refCount: sourceImageIds.length,
|
||||
refCount: references.length,
|
||||
});
|
||||
|
||||
log?.info?.(
|
||||
"IMAGE",
|
||||
`${provider}/${model} (adobe-firefly) | prompt: "${prompt.slice(0, 60)}${prompt.length > 60 ? "..." : ""}"` +
|
||||
(sourceImageIds.length ? ` | refs: ${sourceImageIds.length}/${maxRefs}` : "") +
|
||||
(references.length ? ` | refs: ${references.length}` : "") +
|
||||
` | pollTimeoutMs=${timeoutMs}`
|
||||
);
|
||||
|
||||
@@ -139,9 +136,8 @@ export async function handleAdobeFireflyImageGeneration({
|
||||
aspectRatio: body.aspect_ratio ?? body.aspectRatio ?? body.size,
|
||||
quality: body.quality,
|
||||
seed: Number.isFinite(seed as number) ? (seed as number) : undefined,
|
||||
negativePrompt:
|
||||
typeof body.negative_prompt === "string" ? body.negative_prompt : undefined,
|
||||
sourceImageIds: sourceImageIds.length ? sourceImageIds : undefined,
|
||||
negativePrompt: typeof body.negative_prompt === "string" ? body.negative_prompt : undefined,
|
||||
references: references.length ? references : undefined,
|
||||
sessionCookie,
|
||||
timeoutMs,
|
||||
fetchImpl,
|
||||
|
||||
@@ -10,9 +10,10 @@ import {
|
||||
AdobeFireflyError,
|
||||
adobeFireflyGenerateVideo,
|
||||
resolveAdobeAccessToken,
|
||||
resolveAdobeSourceImageIds,
|
||||
resolveAdobeSourceImageReferences,
|
||||
resolveAdobeVideoModel,
|
||||
} from "../../services/adobeFireflyClient.ts";
|
||||
import { getAdobeReferenceUploadLimit } from "../../services/adobeFireflyModels.ts";
|
||||
|
||||
function normalizePositiveNumber(value: unknown, fallback: number): number {
|
||||
const n = Number(value);
|
||||
@@ -55,7 +56,8 @@ export async function handleAdobeFireflyVideoGeneration({
|
||||
? Number(body.seed)
|
||||
: undefined;
|
||||
// Keep raw paste for Cookie + sherlockToken (x-arp-session-id).
|
||||
const psd = (credentials as { providerSpecificData?: { cookie?: string } })?.providerSpecificData;
|
||||
const psd = (credentials as { providerSpecificData?: { cookie?: string } })
|
||||
?.providerSpecificData;
|
||||
const sessionCookie =
|
||||
(typeof psd?.cookie === "string" && psd.cookie.trim()) ||
|
||||
(typeof credentials?.apiKey === "string" && credentials.apiKey.trim()) ||
|
||||
@@ -63,13 +65,11 @@ export async function handleAdobeFireflyVideoGeneration({
|
||||
? credentials.accessToken
|
||||
: undefined);
|
||||
|
||||
// Kling i2v / Veo ref / Sora frame: upload reference images first.
|
||||
const { id: videoModelId } = resolveAdobeVideoModel(String(model));
|
||||
const maxFrames = videoModelId.includes("kling") || videoModelId.includes("sora") ? 2 : 3;
|
||||
const sourceImageIds = await resolveAdobeSourceImageIds({
|
||||
const { spec } = resolveAdobeVideoModel(String(model));
|
||||
const references = await resolveAdobeSourceImageReferences({
|
||||
accessToken,
|
||||
body,
|
||||
max: maxFrames,
|
||||
max: getAdobeReferenceUploadLimit(spec, "image"),
|
||||
sessionCookie,
|
||||
prompt,
|
||||
fetchImpl,
|
||||
@@ -79,7 +79,7 @@ export async function handleAdobeFireflyVideoGeneration({
|
||||
log?.info?.(
|
||||
"VIDEO",
|
||||
`${provider}/${model} (adobe-firefly) | prompt: "${prompt.slice(0, 60)}${prompt.length > 60 ? "..." : ""}"` +
|
||||
(sourceImageIds.length ? ` | frames: ${sourceImageIds.length}` : "")
|
||||
(references.length ? ` | refs: ${references.length}` : "")
|
||||
);
|
||||
|
||||
const result = await adobeFireflyGenerateVideo({
|
||||
@@ -99,7 +99,7 @@ export async function handleAdobeFireflyVideoGeneration({
|
||||
? body.negativePrompt
|
||||
: undefined,
|
||||
generateAudio: body.generate_audio !== false && body.generateAudio !== false,
|
||||
sourceImageIds: sourceImageIds.length ? sourceImageIds : undefined,
|
||||
references: references.length ? references : undefined,
|
||||
sessionCookie,
|
||||
timeoutMs,
|
||||
fetchImpl,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
8
open-sse/services/adobeFireflyModelSnapshot.ts
Normal file
8
open-sse/services/adobeFireflyModelSnapshot.ts
Normal file
File diff suppressed because one or more lines are too long
@@ -1,328 +1,590 @@
|
||||
/**
|
||||
* Adobe Firefly model catalog: live discovery + static fallback from browser capture.
|
||||
* Adobe Firefly model discovery and normalized media capabilities.
|
||||
*
|
||||
* Live: POST firefly-3p.ff.adobe.io/v2/models/discovery (needs valid IMS token).
|
||||
* Fallback: curated rows from adobe/get_models.txt (2026-07 Firefly SPA capture) so
|
||||
* Media/Models still list usable ids when discovery fails or credentials are missing.
|
||||
* The live discovery schema is authoritative. The generated snapshot is used only
|
||||
* when a request cannot perform authenticated discovery (for example /v1/models).
|
||||
*/
|
||||
|
||||
import {
|
||||
type AdobeFireflyDiscoveredModel,
|
||||
discoverAdobeFireflyModels,
|
||||
resolveAdobeAccessToken,
|
||||
} from "./adobeFireflyClient.ts";
|
||||
import { ADOBE_FIREFLY_DISCOVERY_SNAPSHOT } from "./adobeFireflyModelSnapshot.ts";
|
||||
|
||||
export type AdobeFireflyModality = "image" | "video" | "audio" | "unknown";
|
||||
|
||||
export interface AdobeFireflyDiscoveredModel {
|
||||
modelId: string;
|
||||
modelVersion: string;
|
||||
displayName: string;
|
||||
modality: AdobeFireflyModality;
|
||||
enabled: boolean;
|
||||
providerName?: string;
|
||||
releaseReadiness?: string;
|
||||
healthStatus?: string;
|
||||
inputMediaUseCases: string[];
|
||||
requestSchema?: Record<string, unknown>;
|
||||
backingModel?: string;
|
||||
}
|
||||
|
||||
export interface AdobeFireflyReferenceInputCapability {
|
||||
mediaType: string;
|
||||
usageType: string;
|
||||
minItems: number;
|
||||
maxItems: number | null;
|
||||
maxFileSizeBytes: number | null;
|
||||
}
|
||||
|
||||
export interface AdobeFireflyMediaCapabilities {
|
||||
inputMediaUseCases: string[];
|
||||
schemaProperties: string[];
|
||||
requiredProperties: string[];
|
||||
referenceInputs: AdobeFireflyReferenceInputCapability[];
|
||||
maxReferenceItems: number | null;
|
||||
supportedSizes: string[];
|
||||
supportedAspectRatios: string[];
|
||||
supportedResolutions: string[];
|
||||
supportedDurations: number[];
|
||||
durationMin: number | null;
|
||||
durationMax: number | null;
|
||||
durationDefault: number | null;
|
||||
outputCountMin: number | null;
|
||||
outputCountMax: number | null;
|
||||
promptMaxLength: number | null;
|
||||
releaseReadiness: string;
|
||||
healthStatus: string;
|
||||
}
|
||||
|
||||
export interface AdobeFireflyCatalogModel {
|
||||
/** OpenAI-style id without provider prefix, e.g. nano-banana-pro or flux-fluxPro */
|
||||
/** Stable API id without the provider prefix. */
|
||||
id: string;
|
||||
name: string;
|
||||
modality: "image" | "video";
|
||||
/** Upstream wire modelId for generate-async */
|
||||
upstreamModelId: string;
|
||||
/** Upstream wire modelVersion for generate-async */
|
||||
upstreamModelVersion: string;
|
||||
inputModalities?: string[];
|
||||
providerName: string;
|
||||
backingModel: string;
|
||||
inputModalities: string[];
|
||||
capabilities: AdobeFireflyMediaCapabilities;
|
||||
}
|
||||
|
||||
/**
|
||||
* Static fallback built from adobe/get_models.txt discovery response.
|
||||
* Friendly aliases first (Media page defaults), then popular upstream families.
|
||||
*/
|
||||
export const ADOBE_FIREFLY_FALLBACK_MODELS: AdobeFireflyCatalogModel[] = [
|
||||
// ── Friendly aliases (handler resolveAdobeImageModel / resolveAdobeVideoModel) ──
|
||||
{
|
||||
id: "nano-banana-pro",
|
||||
name: "Gemini 3.0 (Nano Banana Pro)",
|
||||
modality: "image",
|
||||
upstreamModelId: "gemini-flash",
|
||||
upstreamModelVersion: "nano-banana-2",
|
||||
inputModalities: ["text", "image"],
|
||||
},
|
||||
{
|
||||
id: "nano-banana",
|
||||
name: "Gemini 2.5 (Nano Banana)",
|
||||
modality: "image",
|
||||
upstreamModelId: "gemini-flash",
|
||||
upstreamModelVersion: "nano-banana",
|
||||
inputModalities: ["text", "image"],
|
||||
},
|
||||
{
|
||||
id: "nano-banana-2",
|
||||
name: "Gemini 3.1 (Nano Banana 2)",
|
||||
modality: "image",
|
||||
upstreamModelId: "gemini-flash",
|
||||
upstreamModelVersion: "nano-banana-3",
|
||||
inputModalities: ["text", "image"],
|
||||
},
|
||||
{
|
||||
id: "gpt-image-2",
|
||||
name: "GPT Image 2",
|
||||
modality: "image",
|
||||
upstreamModelId: "gpt-image",
|
||||
upstreamModelVersion: "2",
|
||||
inputModalities: ["text", "image"],
|
||||
},
|
||||
{
|
||||
id: "gpt-image",
|
||||
name: "GPT Image 2",
|
||||
modality: "image",
|
||||
upstreamModelId: "gpt-image",
|
||||
upstreamModelVersion: "2",
|
||||
inputModalities: ["text", "image"],
|
||||
},
|
||||
{
|
||||
id: "gpt-image-1.5",
|
||||
name: "GPT Image 1.5",
|
||||
modality: "image",
|
||||
upstreamModelId: "gpt-image",
|
||||
upstreamModelVersion: "1.5",
|
||||
inputModalities: ["text", "image"],
|
||||
},
|
||||
{
|
||||
id: "sora-2",
|
||||
name: "Sora 2",
|
||||
modality: "video",
|
||||
upstreamModelId: "sora",
|
||||
upstreamModelVersion: "sora-2",
|
||||
},
|
||||
{
|
||||
id: "sora-2-pro",
|
||||
name: "Sora 2 Pro",
|
||||
modality: "video",
|
||||
upstreamModelId: "sora",
|
||||
upstreamModelVersion: "sora-2-pro",
|
||||
},
|
||||
{
|
||||
id: "veo-3.1",
|
||||
name: "Veo 3.1",
|
||||
modality: "video",
|
||||
upstreamModelId: "veo",
|
||||
upstreamModelVersion: "3.1-generate",
|
||||
},
|
||||
{
|
||||
id: "veo-3.1-fast",
|
||||
name: "Veo 3.1 Fast",
|
||||
modality: "video",
|
||||
upstreamModelId: "veo",
|
||||
upstreamModelVersion: "3.1-fast-generate",
|
||||
},
|
||||
{
|
||||
id: "veo-3.1-ref",
|
||||
name: "Veo 3.1 Reference",
|
||||
modality: "video",
|
||||
upstreamModelId: "veo",
|
||||
upstreamModelVersion: "3.1-generate",
|
||||
},
|
||||
{
|
||||
id: "kling-3",
|
||||
name: "Kling Video v3 Standard Image to Video",
|
||||
modality: "video",
|
||||
upstreamModelId: "kling",
|
||||
upstreamModelVersion: "kling_v3_standard_i2v",
|
||||
},
|
||||
// ── Additional image families from discovery capture ──
|
||||
{
|
||||
id: "flux-2",
|
||||
name: "Flux 2",
|
||||
modality: "image",
|
||||
upstreamModelId: "flux",
|
||||
upstreamModelVersion: "2",
|
||||
inputModalities: ["text", "image"],
|
||||
},
|
||||
{
|
||||
id: "flux-pro",
|
||||
name: "Flux 1.1 Pro",
|
||||
modality: "image",
|
||||
upstreamModelId: "flux",
|
||||
upstreamModelVersion: "fluxPro",
|
||||
inputModalities: ["text", "image"],
|
||||
},
|
||||
{
|
||||
id: "flux-ultra",
|
||||
name: "Flux 1.1 Ultra",
|
||||
modality: "image",
|
||||
upstreamModelId: "flux",
|
||||
upstreamModelVersion: "fluxUltra",
|
||||
inputModalities: ["text", "image"],
|
||||
},
|
||||
{
|
||||
id: "seedream-4",
|
||||
name: "Seedream 4.0",
|
||||
modality: "image",
|
||||
upstreamModelId: "seedream",
|
||||
upstreamModelVersion: "seedream_v4",
|
||||
inputModalities: ["text", "image"],
|
||||
},
|
||||
{
|
||||
id: "seedream-5-lite",
|
||||
name: "Seedream 5.0 Lite",
|
||||
modality: "image",
|
||||
upstreamModelId: "seedream",
|
||||
upstreamModelVersion: "seedream_v5_lite",
|
||||
inputModalities: ["text", "image"],
|
||||
},
|
||||
{
|
||||
id: "runway-gen4-image",
|
||||
name: "Runway Gen-4 Image",
|
||||
modality: "image",
|
||||
upstreamModelId: "runway-gen4-image",
|
||||
upstreamModelVersion: "gen4_image",
|
||||
inputModalities: ["text", "image"],
|
||||
},
|
||||
// ── Additional video families ──
|
||||
{
|
||||
id: "kling-v3-t2v",
|
||||
name: "Kling Video v3 Standard Text to Video",
|
||||
modality: "video",
|
||||
upstreamModelId: "kling",
|
||||
upstreamModelVersion: "kling_v3_standard_t2v",
|
||||
},
|
||||
{
|
||||
id: "kling-v3-pro-i2v",
|
||||
name: "Kling Video v3 Pro Image to Video",
|
||||
modality: "video",
|
||||
upstreamModelId: "kling",
|
||||
upstreamModelVersion: "kling_v3_pro_i2v",
|
||||
},
|
||||
{
|
||||
id: "luma-ray3",
|
||||
name: "Ray3",
|
||||
modality: "video",
|
||||
upstreamModelId: "luma",
|
||||
upstreamModelVersion: "3.0-ray",
|
||||
},
|
||||
{
|
||||
id: "runway-gen4-turbo",
|
||||
name: "Runway Gen-4 Video",
|
||||
modality: "video",
|
||||
upstreamModelId: "runway",
|
||||
upstreamModelVersion: "gen4_turbo",
|
||||
},
|
||||
];
|
||||
export interface AdobeFireflyImageModelSpec extends AdobeFireflyCatalogModel {
|
||||
modality: "image";
|
||||
/** Payload dialect observed for this model family. */
|
||||
family: "gemini" | "gpt-image" | "generic";
|
||||
}
|
||||
|
||||
/** Stable slug for upstream modelId + modelVersion (catalog id when not a friendly alias). */
|
||||
export interface AdobeFireflyVideoModelSpec extends AdobeFireflyCatalogModel {
|
||||
modality: "video";
|
||||
defaultDuration: number;
|
||||
defaultResolution: string;
|
||||
}
|
||||
|
||||
interface MergedObjectSchema {
|
||||
properties: Record<string, Record<string, unknown>>;
|
||||
required: string[];
|
||||
}
|
||||
|
||||
function asRecord(value: unknown): Record<string, unknown> {
|
||||
return value && typeof value === "object" && !Array.isArray(value)
|
||||
? (value as Record<string, unknown>)
|
||||
: {};
|
||||
}
|
||||
|
||||
function asStringArray(value: unknown): string[] {
|
||||
return Array.isArray(value)
|
||||
? value.map((item) => String(item)).filter((item) => item.length > 0)
|
||||
: [];
|
||||
}
|
||||
|
||||
function finiteInteger(value: unknown): number | null {
|
||||
return Number.isInteger(value) ? (value as number) : null;
|
||||
}
|
||||
|
||||
/** Merge object properties/required keys contributed through JSON Schema allOf. */
|
||||
export function mergeAdobeObjectSchema(schema: unknown): MergedObjectSchema {
|
||||
const merged: MergedObjectSchema = { properties: {}, required: [] };
|
||||
const visit = (value: unknown) => {
|
||||
const node = asRecord(value);
|
||||
const properties = asRecord(node.properties);
|
||||
for (const [key, property] of Object.entries(properties)) {
|
||||
merged.properties[key] = asRecord(property);
|
||||
}
|
||||
merged.required.push(...asStringArray(node.required));
|
||||
if (Array.isArray(node.allOf)) node.allOf.forEach(visit);
|
||||
};
|
||||
visit(schema);
|
||||
merged.required = [...new Set(merged.required)];
|
||||
return merged;
|
||||
}
|
||||
|
||||
function schemaBranches(schema: unknown): Record<string, unknown>[] {
|
||||
const root = asRecord(schema);
|
||||
if (Object.keys(root).length === 0) return [];
|
||||
return [
|
||||
root,
|
||||
...(Array.isArray(root.anyOf) ? root.anyOf.map(asRecord) : []),
|
||||
...(Array.isArray(root.oneOf) ? root.oneOf.map(asRecord) : []),
|
||||
];
|
||||
}
|
||||
|
||||
function enumStrings(schema: unknown): string[] {
|
||||
return [
|
||||
...new Set(
|
||||
schemaBranches(schema)
|
||||
.flatMap((branch) => (Array.isArray(branch.enum) ? branch.enum : []))
|
||||
.filter((value): value is string => typeof value === "string")
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
function integerBranch(schema: unknown): Record<string, unknown> {
|
||||
return schemaBranches(schema).find((branch) => branch.type === "integer") || {};
|
||||
}
|
||||
|
||||
/** Stable, collision-resistant public id for an exact upstream model/version pair. */
|
||||
export function slugifyAdobeModel(modelId: string, modelVersion: string): string {
|
||||
const mid = String(modelId || "")
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, "-")
|
||||
.replace(/^-|-$/g, "");
|
||||
const ver = String(modelVersion || "")
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9.]+/g, "-")
|
||||
.replace(/^-|-$/g, "");
|
||||
if (!ver || ver === "default" || ver === mid) return mid || "model";
|
||||
return `${mid}-${ver}`;
|
||||
const slug = (value: string, allowDot = false) =>
|
||||
String(value || "")
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(allowDot ? /[^a-z0-9.]+/g : /[^a-z0-9]+/g, "-")
|
||||
.replace(/^-|-$/g, "");
|
||||
const family = slug(modelId);
|
||||
// Adobe still uses `kling_v3_omni*` internally, while discovery exposes these
|
||||
// products to users as Kling O3. Never leak the obsolete/internal "omni" name
|
||||
// into the public API catalog; the untouched upstream version stays in the spec.
|
||||
const publicVersion =
|
||||
family === "kling" ? modelVersion.replace(/^kling_v3_omni/i, "kling_o3") : modelVersion;
|
||||
const version = slug(publicVersion, true);
|
||||
if (!version || version === "default" || version === family) return family || "model";
|
||||
return `${family}-${version}`;
|
||||
}
|
||||
|
||||
/** Map discovery rows → catalog entries (image/video only). */
|
||||
export function mapDiscoveredToCatalog(
|
||||
rows: AdobeFireflyDiscoveredModel[]
|
||||
): AdobeFireflyCatalogModel[] {
|
||||
const out: AdobeFireflyCatalogModel[] = [];
|
||||
const seen = new Set<string>();
|
||||
/** Parse POST /v2/models/discovery without discarding its resolved request schema. */
|
||||
export function parseAdobeModelsDiscovery(body: unknown): AdobeFireflyDiscoveredModel[] {
|
||||
const root = asRecord(body);
|
||||
const families = Array.isArray(root.models) ? root.models : [];
|
||||
const rows: AdobeFireflyDiscoveredModel[] = [];
|
||||
|
||||
// Prefer friendly aliases when upstream matches known fallback rows.
|
||||
for (const fb of ADOBE_FIREFLY_FALLBACK_MODELS) {
|
||||
const hit = rows.find(
|
||||
(r) =>
|
||||
r.modelId === fb.upstreamModelId &&
|
||||
r.modelVersion === fb.upstreamModelVersion &&
|
||||
(r.modality === fb.modality || r.modality === "unknown")
|
||||
);
|
||||
if (hit && !seen.has(fb.id)) {
|
||||
seen.add(fb.id);
|
||||
out.push({
|
||||
...fb,
|
||||
name: hit.displayName || fb.name,
|
||||
for (const familyValue of families) {
|
||||
const family = asRecord(familyValue);
|
||||
const modelId = String(family.modelId || "").trim();
|
||||
if (!modelId) continue;
|
||||
for (const [modelVersion, versionValue] of Object.entries(asRecord(family.modelVersions))) {
|
||||
const version = asRecord(versionValue);
|
||||
if (version.enabled === false) continue;
|
||||
const outputModalities = asStringArray(version.outputModality).map((item) =>
|
||||
item.toLowerCase()
|
||||
);
|
||||
const modality: AdobeFireflyModality = outputModalities.includes("image")
|
||||
? "image"
|
||||
: outputModalities.includes("video")
|
||||
? "video"
|
||||
: outputModalities.includes("audio")
|
||||
? "audio"
|
||||
: "unknown";
|
||||
rows.push({
|
||||
modelId,
|
||||
modelVersion,
|
||||
displayName: String(
|
||||
version.modelDisplayName || version.modelCaiDisplayName || modelVersion
|
||||
),
|
||||
modality,
|
||||
enabled: version.enabled !== false,
|
||||
providerName:
|
||||
typeof family.acModelFamilyProviderDisplayName === "string"
|
||||
? family.acModelFamilyProviderDisplayName
|
||||
: undefined,
|
||||
releaseReadiness:
|
||||
typeof version.releaseReadiness === "string" ? version.releaseReadiness : undefined,
|
||||
healthStatus: typeof version.healthStatus === "string" ? version.healthStatus : undefined,
|
||||
inputMediaUseCases: asStringArray(version.inputMediaUseCase),
|
||||
requestSchema: asRecord(version.requestSchema),
|
||||
backingModel:
|
||||
typeof version.bksGenerationModel === "string" ? version.bksGenerationModel : undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
function normalizeCapabilities(row: AdobeFireflyDiscoveredModel): AdobeFireflyMediaCapabilities {
|
||||
const schema = mergeAdobeObjectSchema(row.requestSchema);
|
||||
const referenceSchema = asRecord(schema.properties.referenceBlobs);
|
||||
const referenceInputs: AdobeFireflyReferenceInputCapability[] = [];
|
||||
const mediaCapabilities = Array.isArray(referenceSchema["x-capabilities"])
|
||||
? referenceSchema["x-capabilities"]
|
||||
: [];
|
||||
for (const mediaValue of mediaCapabilities) {
|
||||
const media = asRecord(mediaValue);
|
||||
const maxFileSizeBytes = finiteInteger(media.maxFileSizeBytes);
|
||||
const usageConstraints = Array.isArray(media.usageConstraints) ? media.usageConstraints : [];
|
||||
for (const usageValue of usageConstraints) {
|
||||
const usage = asRecord(usageValue);
|
||||
if (usage.deprecated === true) continue;
|
||||
const usageType = String(usage.usageType || "");
|
||||
const mediaType = String(media.mediaType || "");
|
||||
if (!usageType || !mediaType) continue;
|
||||
referenceInputs.push({
|
||||
mediaType,
|
||||
usageType,
|
||||
minItems: finiteInteger(usage.minItems) ?? 0,
|
||||
maxItems: finiteInteger(usage.maxItems),
|
||||
maxFileSizeBytes,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (const r of rows) {
|
||||
if (r.modality !== "image" && r.modality !== "video") continue;
|
||||
const id = slugifyAdobeModel(r.modelId, r.modelVersion);
|
||||
if (seen.has(id)) continue;
|
||||
// Skip if already covered by a friendly alias with same upstream
|
||||
if (
|
||||
out.some(
|
||||
(o) =>
|
||||
o.upstreamModelId === r.modelId && o.upstreamModelVersion === r.modelVersion
|
||||
const supportedSizes = [
|
||||
...new Set(
|
||||
schemaBranches(schema.properties.size)
|
||||
.flatMap((branch) => (Array.isArray(branch.enum) ? branch.enum : []))
|
||||
.map(asRecord)
|
||||
.filter((size) => finiteInteger(size.width) !== null && finiteInteger(size.height) !== null)
|
||||
.map((size) => `${size.width}x${size.height}`)
|
||||
),
|
||||
];
|
||||
const supportedAspectRatios = [
|
||||
...new Set(
|
||||
schemaBranches(schema.properties.generationSettings).flatMap((branch) =>
|
||||
enumStrings(asRecord(asRecord(branch.properties).aspectRatio))
|
||||
)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
seen.add(id);
|
||||
out.push({
|
||||
id,
|
||||
name: r.displayName || id,
|
||||
modality: r.modality,
|
||||
upstreamModelId: r.modelId,
|
||||
upstreamModelVersion: r.modelVersion,
|
||||
inputModalities: r.modality === "image" ? ["text", "image"] : ["text"],
|
||||
});
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
export function getAdobeFireflyFallbackCatalog(modality?: "image" | "video"): AdobeFireflyCatalogModel[] {
|
||||
if (!modality) return [...ADOBE_FIREFLY_FALLBACK_MODELS];
|
||||
return ADOBE_FIREFLY_FALLBACK_MODELS.filter((m) => m.modality === modality);
|
||||
}
|
||||
|
||||
/**
|
||||
* Live discovery when credentials resolve; otherwise static fallback from get_models capture.
|
||||
*/
|
||||
export async function resolveAdobeFireflyCatalog(opts: {
|
||||
credentials?: {
|
||||
apiKey?: string;
|
||||
accessToken?: string;
|
||||
providerSpecificData?: Record<string, unknown> | null;
|
||||
} | null;
|
||||
modality?: "image" | "video";
|
||||
fetchImpl?: typeof fetch;
|
||||
}): Promise<{ models: AdobeFireflyCatalogModel[]; source: "api" | "fallback" }> {
|
||||
const fetchImpl = opts.fetchImpl || fetch;
|
||||
try {
|
||||
if (opts.credentials) {
|
||||
const token = await resolveAdobeAccessToken(opts.credentials, fetchImpl);
|
||||
const discovered = await discoverAdobeFireflyModels(token, fetchImpl);
|
||||
let catalog = mapDiscoveredToCatalog(discovered);
|
||||
if (opts.modality) catalog = catalog.filter((m) => m.modality === opts.modality);
|
||||
if (catalog.length > 0) return { models: catalog, source: "api" };
|
||||
}
|
||||
} catch {
|
||||
// fall through to static catalog
|
||||
}
|
||||
),
|
||||
];
|
||||
const duration = integerBranch(schema.properties.duration);
|
||||
const outputCount = integerBranch(schema.properties.n);
|
||||
const prompt =
|
||||
schemaBranches(schema.properties.prompt).find((branch) => branch.type === "string") || {};
|
||||
|
||||
return {
|
||||
models: getAdobeFireflyFallbackCatalog(opts.modality),
|
||||
source: "fallback",
|
||||
inputMediaUseCases: [...row.inputMediaUseCases],
|
||||
schemaProperties: Object.keys(schema.properties),
|
||||
requiredProperties: [...schema.required],
|
||||
referenceInputs,
|
||||
maxReferenceItems: finiteInteger(referenceSchema.maxItems),
|
||||
supportedSizes,
|
||||
supportedAspectRatios,
|
||||
supportedResolutions: enumStrings(schema.properties.resolution),
|
||||
supportedDurations: [
|
||||
...new Set(
|
||||
schemaBranches(schema.properties.duration)
|
||||
.flatMap((branch) => (Array.isArray(branch.enum) ? branch.enum : []))
|
||||
.filter((value): value is number => Number.isInteger(value))
|
||||
),
|
||||
],
|
||||
durationMin: finiteInteger(duration.minimum),
|
||||
durationMax: finiteInteger(duration.maximum),
|
||||
durationDefault: finiteInteger(duration.default),
|
||||
outputCountMin: finiteInteger(outputCount.minimum),
|
||||
outputCountMax: finiteInteger(outputCount.maximum),
|
||||
promptMaxLength: finiteInteger(prompt.maxLength),
|
||||
releaseReadiness: row.releaseReadiness || "",
|
||||
healthStatus: row.healthStatus || "",
|
||||
};
|
||||
}
|
||||
|
||||
/** Registry-shaped models for imageRegistry / videoRegistry. */
|
||||
export function toRegistryImageModels(
|
||||
models: AdobeFireflyCatalogModel[] = getAdobeFireflyFallbackCatalog("image")
|
||||
): Array<{ id: string; name: string; inputModalities?: string[] }> {
|
||||
return models
|
||||
.filter((m) => m.modality === "image")
|
||||
.map((m) => ({
|
||||
id: m.id,
|
||||
name: m.name.startsWith("Firefly ") ? m.name : `Firefly ${m.name}`,
|
||||
inputModalities: m.inputModalities || ["text", "image"],
|
||||
}));
|
||||
function isCallableGenerationModel(row: AdobeFireflyDiscoveredModel): boolean {
|
||||
if (row.modality !== "image" && row.modality !== "video") return false;
|
||||
if (!mergeAdobeObjectSchema(row.requestSchema).properties.prompt) return false;
|
||||
const excluded = new Set(["upscaling", "sharpening", "denoising"]);
|
||||
return !row.inputMediaUseCases.some((value) => excluded.has(value.toLowerCase()));
|
||||
}
|
||||
|
||||
export function toRegistryVideoModels(
|
||||
models: AdobeFireflyCatalogModel[] = getAdobeFireflyFallbackCatalog("video")
|
||||
): Array<{ id: string; name: string }> {
|
||||
return models
|
||||
.filter((m) => m.modality === "video")
|
||||
.map((m) => ({
|
||||
id: m.id,
|
||||
name: m.name.startsWith("Firefly ") ? m.name : `Firefly ${m.name}`,
|
||||
}));
|
||||
function deriveInputModalities(capabilities: AdobeFireflyMediaCapabilities): string[] {
|
||||
return ["text", ...new Set(capabilities.referenceInputs.map((reference) => reference.mediaType))];
|
||||
}
|
||||
|
||||
function semanticCatalogKey(model: AdobeFireflyCatalogModel): string {
|
||||
return JSON.stringify({
|
||||
backingModel: model.backingModel,
|
||||
name: model.name,
|
||||
modality: model.modality,
|
||||
capabilities: model.capabilities,
|
||||
});
|
||||
}
|
||||
|
||||
/** Normalize and de-duplicate callable image/video rows from live discovery. */
|
||||
export function mapDiscoveredToCatalog(
|
||||
rows: AdobeFireflyDiscoveredModel[]
|
||||
): AdobeFireflyCatalogModel[] {
|
||||
const output: AdobeFireflyCatalogModel[] = [];
|
||||
const seen = new Set<string>();
|
||||
for (const row of rows) {
|
||||
if (!isCallableGenerationModel(row)) continue;
|
||||
const capabilities = normalizeCapabilities(row);
|
||||
const model: AdobeFireflyCatalogModel = {
|
||||
id: slugifyAdobeModel(row.modelId, row.modelVersion),
|
||||
name: row.displayName,
|
||||
modality: row.modality as "image" | "video",
|
||||
upstreamModelId: row.modelId,
|
||||
upstreamModelVersion: row.modelVersion,
|
||||
providerName: row.providerName || "",
|
||||
backingModel: row.backingModel || "",
|
||||
inputModalities: deriveInputModalities(capabilities),
|
||||
capabilities,
|
||||
};
|
||||
const key = semanticCatalogKey(model);
|
||||
if (seen.has(key)) continue;
|
||||
seen.add(key);
|
||||
output.push(model);
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
function snapshotCatalog(): AdobeFireflyCatalogModel[] {
|
||||
return ADOBE_FIREFLY_DISCOVERY_SNAPSHOT.map((model) => {
|
||||
const capabilities: AdobeFireflyMediaCapabilities = {
|
||||
inputMediaUseCases: [...model.inputMediaUseCases],
|
||||
schemaProperties: [...model.schemaProperties],
|
||||
requiredProperties: [...model.requiredProperties],
|
||||
referenceInputs: model.referenceInputs.map((reference) => ({ ...reference })),
|
||||
maxReferenceItems: model.maxReferenceItems,
|
||||
supportedSizes: [...model.supportedSizes],
|
||||
supportedAspectRatios: [...model.supportedAspectRatios],
|
||||
supportedResolutions: [...model.supportedResolutions],
|
||||
supportedDurations: [...model.supportedDurations],
|
||||
durationMin: model.durationMin,
|
||||
durationMax: model.durationMax,
|
||||
durationDefault: model.durationDefault,
|
||||
outputCountMin: model.outputCountMin,
|
||||
outputCountMax: model.outputCountMax,
|
||||
promptMaxLength: model.promptMaxLength,
|
||||
releaseReadiness: model.releaseReadiness,
|
||||
healthStatus: model.healthStatus,
|
||||
};
|
||||
return {
|
||||
id: model.id,
|
||||
name: model.name,
|
||||
modality: model.modality,
|
||||
upstreamModelId: model.upstreamModelId,
|
||||
upstreamModelVersion: model.upstreamModelVersion,
|
||||
providerName: model.providerName,
|
||||
backingModel: model.backingModel,
|
||||
inputModalities: deriveInputModalities(capabilities),
|
||||
capabilities,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export const ADOBE_FIREFLY_FALLBACK_MODELS: AdobeFireflyCatalogModel[] = snapshotCatalog();
|
||||
|
||||
export function getAdobeFireflyFallbackCatalog(
|
||||
modality?: "image" | "video"
|
||||
): AdobeFireflyCatalogModel[] {
|
||||
return ADOBE_FIREFLY_FALLBACK_MODELS.filter((model) => !modality || model.modality === modality);
|
||||
}
|
||||
|
||||
function imageFamily(model: AdobeFireflyCatalogModel): AdobeFireflyImageModelSpec["family"] {
|
||||
if (model.upstreamModelId === "gemini-flash") return "gemini";
|
||||
if (model.upstreamModelId === "gpt-image" || model.upstreamModelId === "gpt-4o-image") {
|
||||
return "gpt-image";
|
||||
}
|
||||
return "generic";
|
||||
}
|
||||
|
||||
export const ADOBE_FIREFLY_IMAGE_MODELS: Record<string, AdobeFireflyImageModelSpec> =
|
||||
Object.fromEntries(
|
||||
getAdobeFireflyFallbackCatalog("image").map((model) => [
|
||||
model.id,
|
||||
{ ...model, modality: "image" as const, family: imageFamily(model) },
|
||||
])
|
||||
);
|
||||
|
||||
function defaultDuration(model: AdobeFireflyCatalogModel): number {
|
||||
const caps = model.capabilities;
|
||||
return caps.durationDefault ?? caps.supportedDurations[0] ?? caps.durationMin ?? 5;
|
||||
}
|
||||
|
||||
function defaultResolution(model: AdobeFireflyCatalogModel): string {
|
||||
if (model.capabilities.supportedSizes.some((value) => value.includes("1920x1080"))) {
|
||||
return "1080p";
|
||||
}
|
||||
return "720p";
|
||||
}
|
||||
|
||||
export const ADOBE_FIREFLY_VIDEO_MODELS: Record<string, AdobeFireflyVideoModelSpec> =
|
||||
Object.fromEntries(
|
||||
getAdobeFireflyFallbackCatalog("video").map((model) => [
|
||||
model.id,
|
||||
{
|
||||
...model,
|
||||
modality: "video" as const,
|
||||
defaultDuration: defaultDuration(model),
|
||||
defaultResolution: defaultResolution(model),
|
||||
},
|
||||
])
|
||||
);
|
||||
|
||||
const LEGACY_MODEL_ALIASES: Record<string, string> = {
|
||||
"nano-banana": "gemini-flash-nano-banana",
|
||||
"nano-banana-pro": "gemini-flash-nano-banana-2",
|
||||
"nano-banana-2": "gemini-flash-nano-banana-3",
|
||||
"gpt-image": "gpt-image-2",
|
||||
"gpt-image-2": "gpt-image-2",
|
||||
"gpt-image-1.5": "gpt-image-1.5",
|
||||
"flux-2": "flux-2",
|
||||
"flux-pro": "flux-fluxpro",
|
||||
"flux-ultra": "flux-fluxultra",
|
||||
"seedream-4": "seedream-seedream-v4",
|
||||
"seedream-5-lite": "seedream-seedream-v5-lite",
|
||||
"runway-gen4-image": "runway-gen4-image",
|
||||
"veo-3.1": "veo-3.1-generate",
|
||||
"veo-3.1-fast": "veo-3.1-fast-generate",
|
||||
"luma-ray3": "luma-3.0-ray",
|
||||
"runway-gen4-turbo": "runway-gen4-turbo",
|
||||
// Backward compatibility only; the catalog advertises the exact discovered id.
|
||||
"kling-3": "kling-kling-v3-standard-i2v",
|
||||
};
|
||||
|
||||
// Preserve established API aliases when (and only when) they resolve to a model
|
||||
// that is present in the verified discovery snapshot. These keys are not listed.
|
||||
for (const [alias, target] of Object.entries(LEGACY_MODEL_ALIASES)) {
|
||||
const imageTarget = ADOBE_FIREFLY_IMAGE_MODELS[target];
|
||||
if (imageTarget) ADOBE_FIREFLY_IMAGE_MODELS[alias] = imageTarget;
|
||||
const videoTarget = ADOBE_FIREFLY_VIDEO_MODELS[target];
|
||||
if (videoTarget) ADOBE_FIREFLY_VIDEO_MODELS[alias] = videoTarget;
|
||||
}
|
||||
|
||||
/** Backward-compatible request ids. Kept out of every advertised model catalog. */
|
||||
export const ADOBE_FIREFLY_IMAGE_ROUTING_ALIASES = Object.freeze(
|
||||
Object.entries(LEGACY_MODEL_ALIASES)
|
||||
.filter(([, target]) => Boolean(ADOBE_FIREFLY_IMAGE_MODELS[target]))
|
||||
.map(([alias]) => alias)
|
||||
);
|
||||
|
||||
function normalizeRequestedId(model: string): string {
|
||||
return String(model || "")
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/^adobe-firefly\//, "")
|
||||
.replace(/^firefly\//, "");
|
||||
}
|
||||
|
||||
function resolveCatalogId(model: string): string {
|
||||
const requested = normalizeRequestedId(model);
|
||||
return LEGACY_MODEL_ALIASES[requested] || requested;
|
||||
}
|
||||
|
||||
export function resolveAdobeImageModel(model: string): {
|
||||
id: string;
|
||||
spec: AdobeFireflyImageModelSpec;
|
||||
} {
|
||||
const id = resolveCatalogId(model);
|
||||
const spec = ADOBE_FIREFLY_IMAGE_MODELS[id];
|
||||
if (!spec) {
|
||||
throw new Error(
|
||||
`Unknown Adobe Firefly image model: ${normalizeRequestedId(model) || "(empty)"}`
|
||||
);
|
||||
}
|
||||
return { id, spec };
|
||||
}
|
||||
|
||||
export function resolveAdobeVideoModel(model: string): {
|
||||
id: string;
|
||||
spec: AdobeFireflyVideoModelSpec;
|
||||
} {
|
||||
const id = resolveCatalogId(model);
|
||||
const spec = ADOBE_FIREFLY_VIDEO_MODELS[id];
|
||||
if (!spec) {
|
||||
throw new Error(
|
||||
`Unknown Adobe Firefly video model: ${normalizeRequestedId(model) || "(empty)"}`
|
||||
);
|
||||
}
|
||||
return { id, spec };
|
||||
}
|
||||
|
||||
export function toRegistryImageModels(): Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
inputModalities: string[];
|
||||
imageRequired?: boolean;
|
||||
supportedSizes: string[];
|
||||
mediaCapabilities: Record<string, unknown>;
|
||||
}> {
|
||||
const generated = getAdobeFireflyFallbackCatalog("image").map((model) => ({
|
||||
id: model.id,
|
||||
name: `Firefly ${model.name}`,
|
||||
inputModalities: model.inputModalities,
|
||||
supportedSizes: model.capabilities.supportedSizes,
|
||||
mediaCapabilities: toAdobeMediaCapabilitiesApi(model),
|
||||
}));
|
||||
// Upscaling uses a distinct Firefly endpoint and is not returned by the image
|
||||
// generation discovery schema. Keep its two supported Topaz models visible in
|
||||
// the same provider catalog so image clients can select them deliberately.
|
||||
return [
|
||||
...generated,
|
||||
{
|
||||
id: "topaz-standard",
|
||||
name: "Firefly Topaz Upscale (Standard)",
|
||||
inputModalities: ["image"],
|
||||
imageRequired: true,
|
||||
supportedSizes: [],
|
||||
mediaCapabilities: { input_media_use_cases: ["upscaling"] },
|
||||
},
|
||||
{
|
||||
id: "topaz-bloom",
|
||||
name: "Firefly Topaz Bloom (Creative Upscale)",
|
||||
inputModalities: ["image"],
|
||||
imageRequired: true,
|
||||
supportedSizes: [],
|
||||
mediaCapabilities: { input_media_use_cases: ["upscaling"] },
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export function toRegistryVideoModels(): Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
supportedSizes: string[];
|
||||
mediaCapabilities: Record<string, unknown>;
|
||||
}> {
|
||||
return getAdobeFireflyFallbackCatalog("video").map((model) => ({
|
||||
id: model.id,
|
||||
name: `Firefly ${model.name}`,
|
||||
supportedSizes: model.capabilities.supportedSizes,
|
||||
mediaCapabilities: toAdobeMediaCapabilitiesApi(model),
|
||||
}));
|
||||
}
|
||||
|
||||
/** JSON-safe extension emitted by /v1/models. */
|
||||
export function toAdobeMediaCapabilitiesApi(
|
||||
model: AdobeFireflyCatalogModel
|
||||
): Record<string, unknown> {
|
||||
const caps = model.capabilities;
|
||||
return {
|
||||
upstream_model_id: model.upstreamModelId,
|
||||
upstream_model_version: model.upstreamModelVersion,
|
||||
provider_name: model.providerName,
|
||||
release_readiness: caps.releaseReadiness,
|
||||
health_status: caps.healthStatus,
|
||||
input_media_use_cases: caps.inputMediaUseCases,
|
||||
reference_inputs: caps.referenceInputs.map((reference) => ({
|
||||
media_type: reference.mediaType,
|
||||
usage_type: reference.usageType,
|
||||
min_items: reference.minItems,
|
||||
max_items: reference.maxItems,
|
||||
max_file_size_bytes: reference.maxFileSizeBytes,
|
||||
})),
|
||||
max_reference_items: caps.maxReferenceItems,
|
||||
supported_sizes: caps.supportedSizes,
|
||||
supported_aspect_ratios: caps.supportedAspectRatios,
|
||||
supported_resolutions: caps.supportedResolutions,
|
||||
supported_durations: caps.supportedDurations,
|
||||
duration_min: caps.durationMin,
|
||||
duration_max: caps.durationMax,
|
||||
duration_default: caps.durationDefault,
|
||||
output_count_min: caps.outputCountMin,
|
||||
output_count_max: caps.outputCountMax,
|
||||
prompt_max_length: caps.promptMaxLength,
|
||||
};
|
||||
}
|
||||
|
||||
export function getAdobeReferenceUploadLimit(
|
||||
model: AdobeFireflyCatalogModel,
|
||||
mediaType: string
|
||||
): number {
|
||||
if (model.capabilities.maxReferenceItems !== null) {
|
||||
return Math.max(1, Math.min(32, model.capabilities.maxReferenceItems));
|
||||
}
|
||||
const declaredTotal = model.capabilities.referenceInputs
|
||||
.filter((reference) => reference.mediaType === mediaType)
|
||||
.reduce((total, reference) => total + (reference.maxItems ?? 0), 0);
|
||||
return Math.max(1, Math.min(32, declaredTotal || 1));
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ import { getResolvedModelCapabilities } from "@/lib/modelCapabilities";
|
||||
import { isVisionModelId } from "@/shared/constants/visionModels";
|
||||
|
||||
export type AutoCategory = "coding" | "reasoning" | "vision" | "chat" | "multimodal";
|
||||
export type AutoTier = "fast" | "cheap" | "floor" | "free" | "reliable" | "pro";
|
||||
export type AutoTier = "fast" | "cheap" | "floor" | "free" | "reliable" | "pro" | "subscription";
|
||||
|
||||
export const AUTO_CATEGORIES: readonly AutoCategory[] = [
|
||||
"coding",
|
||||
@@ -38,6 +38,7 @@ export const AUTO_TIERS: readonly AutoTier[] = [
|
||||
"free",
|
||||
"reliable",
|
||||
"pro",
|
||||
"subscription",
|
||||
];
|
||||
|
||||
const CATEGORY_SET = new Set<string>(AUTO_CATEGORIES);
|
||||
@@ -86,6 +87,8 @@ export function tierToWeightVariant(tier?: AutoTier): AutoVariant | "reliability
|
||||
return "cheap";
|
||||
case "reliable":
|
||||
return "reliability";
|
||||
case "subscription":
|
||||
return "coding"; // subscription uses quality-first weights
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
@@ -133,6 +136,9 @@ export function buildAutoCandidateFilter(
|
||||
if (tier === "pro") {
|
||||
checks.push((c) => safeClassifyTier(c) === "premium");
|
||||
}
|
||||
if (tier === "subscription") {
|
||||
checks.push((c) => safeClassifyTier(c) === "subscription");
|
||||
}
|
||||
|
||||
if (checks.length === 0) return null;
|
||||
return (candidate: PoolCandidate) => checks.every((fn) => fn(candidate));
|
||||
|
||||
@@ -25,6 +25,26 @@ export function classifyTier(provider: string, model: string): TierAssignment {
|
||||
return tierCache.get(key)!;
|
||||
}
|
||||
|
||||
// Check for Claude Code subscription models first
|
||||
if (
|
||||
provider === "cc" ||
|
||||
provider === "claude-code" ||
|
||||
model.startsWith("cc/") ||
|
||||
model.startsWith("claude-code/")
|
||||
) {
|
||||
const assignment: TierAssignment = {
|
||||
provider,
|
||||
model,
|
||||
tier: PROVIDER_TIER.SUBSCRIPTION,
|
||||
reason: `Claude Code subscription model detected: ${provider}/${model}`,
|
||||
costPer1MInput: 0,
|
||||
costPer1MOutput: 0,
|
||||
hasFreeTier: false,
|
||||
};
|
||||
tierCache.set(key, assignment);
|
||||
return assignment;
|
||||
}
|
||||
|
||||
if (isExplicitlyFree(provider, currentConfig)) {
|
||||
const assignment: TierAssignment = {
|
||||
provider,
|
||||
|
||||
@@ -2,6 +2,7 @@ export const PROVIDER_TIER = {
|
||||
FREE: "free",
|
||||
CHEAP: "cheap",
|
||||
PREMIUM: "premium",
|
||||
SUBSCRIPTION: "subscription",
|
||||
} as const;
|
||||
|
||||
export type ProviderTier = (typeof PROVIDER_TIER)[keyof typeof PROVIDER_TIER];
|
||||
|
||||
@@ -309,6 +309,7 @@
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-reconciler": "^0.33.0",
|
||||
"recharts": "^3.8.1",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"safe-regex": "^2.1.1",
|
||||
"selfsigned": "^5.5.0",
|
||||
"smol-toml": "1.7.1",
|
||||
|
||||
477
pm2.log
Normal file
477
pm2.log
Normal file
@@ -0,0 +1,477 @@
|
||||
[1m[90m[TAILING] Tailing last 200 lines for [omniroute] process (change the value with --lines option)[39m[22m
|
||||
[90m/Users/work/.pm2/logs/omniroute-error.log last 200 lines:[39m
|
||||
[31m21|omnirou | [39m- Local: http://localhost:3001
|
||||
[31m21|omnirou | [39m- PID: 20496
|
||||
[31m21|omnirou | [39m- Dir: /Users/work/serpentos/Hermes_Omni
|
||||
[31m21|omnirou | [39m- Log: .build/next/dev/logs/next-development.log
|
||||
[31m21|omnirou | [39m
|
||||
[31m21|omnirou | [39mYou can access the existing server at http://localhost:3001,
|
||||
[31m21|omnirou | [39mor run kill 20496 to stop it and start a new one.
|
||||
[31m21|omnirou | [39mnpm notice run omniroute@3.8.50 dev
|
||||
[31m21|omnirou | [39mnpm notice run node --max-old-space-size=8192 scripts/dev/run-next.mjs dev --port 20128
|
||||
[31m21|omnirou | [39m[bootstrap] ℹ️ The following OAuth integrations are not configured:
|
||||
[31m21|omnirou | [39m[bootstrap] • Antigravity OAuth (ANTIGRAVITY_OAUTH_CLIENT_SECRET) — set in .env or /Users/work/.omniroute/server.env
|
||||
[31m21|omnirou | [39m[bootstrap] • Qoder OAuth (QODER_OAUTH_CLIENT_SECRET) — set in .env or /Users/work/.omniroute/server.env
|
||||
[31m21|omnirou | [39m[bootstrap] These providers will not work until configured.
|
||||
[31m21|omnirou | [39m[bootstrap] ⚠️ INITIAL_PASSWORD is not set — using default 'CHANGEME'. Change it in Settings!
|
||||
[31m21|omnirou | [39m⨯ Another next dev server is already running.
|
||||
[31m21|omnirou | [39m
|
||||
[31m21|omnirou | [39m- Local: http://localhost:3001
|
||||
[31m21|omnirou | [39m- PID: 20496
|
||||
[31m21|omnirou | [39m- Dir: /Users/work/serpentos/Hermes_Omni
|
||||
[31m21|omnirou | [39m- Log: .build/next/dev/logs/next-development.log
|
||||
[31m21|omnirou | [39m
|
||||
[31m21|omnirou | [39mYou can access the existing server at http://localhost:3001,
|
||||
[31m21|omnirou | [39mor run kill 20496 to stop it and start a new one.
|
||||
[31m21|omnirou | [39mnpm notice run omniroute@3.8.50 dev
|
||||
[31m21|omnirou | [39mnpm notice run node --max-old-space-size=8192 scripts/dev/run-next.mjs dev --port 20128
|
||||
[31m21|omnirou | [39m[bootstrap] ℹ️ The following OAuth integrations are not configured:
|
||||
[31m21|omnirou | [39m[bootstrap] • Antigravity OAuth (ANTIGRAVITY_OAUTH_CLIENT_SECRET) — set in .env or /Users/work/.omniroute/server.env
|
||||
[31m21|omnirou | [39m[bootstrap] • Qoder OAuth (QODER_OAUTH_CLIENT_SECRET) — set in .env or /Users/work/.omniroute/server.env
|
||||
[31m21|omnirou | [39m[bootstrap] These providers will not work until configured.
|
||||
[31m21|omnirou | [39m[bootstrap] ⚠️ INITIAL_PASSWORD is not set — using default 'CHANGEME'. Change it in Settings!
|
||||
[31m21|omnirou | [39m⨯ Another next dev server is already running.
|
||||
[31m21|omnirou | [39m
|
||||
[31m21|omnirou | [39m- Local: http://localhost:3001
|
||||
[31m21|omnirou | [39m- PID: 20496
|
||||
[31m21|omnirou | [39m- Dir: /Users/work/serpentos/Hermes_Omni
|
||||
[31m21|omnirou | [39m- Log: .build/next/dev/logs/next-development.log
|
||||
[31m21|omnirou | [39m
|
||||
[31m21|omnirou | [39mYou can access the existing server at http://localhost:3001,
|
||||
[31m21|omnirou | [39mor run kill 20496 to stop it and start a new one.
|
||||
[31m21|omnirou | [39mnpm notice run omniroute@3.8.50 dev
|
||||
[31m21|omnirou | [39mnpm notice run node --max-old-space-size=8192 scripts/dev/run-next.mjs dev --port 20128
|
||||
[31m21|omnirou | [39m[bootstrap] ℹ️ The following OAuth integrations are not configured:
|
||||
[31m21|omnirou | [39m[bootstrap] • Antigravity OAuth (ANTIGRAVITY_OAUTH_CLIENT_SECRET) — set in .env or /Users/work/.omniroute/server.env
|
||||
[31m21|omnirou | [39m[bootstrap] • Qoder OAuth (QODER_OAUTH_CLIENT_SECRET) — set in .env or /Users/work/.omniroute/server.env
|
||||
[31m21|omnirou | [39m[bootstrap] These providers will not work until configured.
|
||||
[31m21|omnirou | [39m[bootstrap] ⚠️ INITIAL_PASSWORD is not set — using default 'CHANGEME'. Change it in Settings!
|
||||
[31m21|omnirou | [39m⨯ Another next dev server is already running.
|
||||
[31m21|omnirou | [39m
|
||||
[31m21|omnirou | [39m- Local: http://localhost:3001
|
||||
[31m21|omnirou | [39m- PID: 20496
|
||||
[31m21|omnirou | [39m- Dir: /Users/work/serpentos/Hermes_Omni
|
||||
[31m21|omnirou | [39m- Log: .build/next/dev/logs/next-development.log
|
||||
[31m21|omnirou | [39m
|
||||
[31m21|omnirou | [39mYou can access the existing server at http://localhost:3001,
|
||||
[31m21|omnirou | [39mor run kill 20496 to stop it and start a new one.
|
||||
[31m21|omnirou | [39mnpm notice run omniroute@3.8.50 dev
|
||||
[31m21|omnirou | [39mnpm notice run node --max-old-space-size=8192 scripts/dev/run-next.mjs dev --port 20128
|
||||
[31m21|omnirou | [39m[bootstrap] ℹ️ The following OAuth integrations are not configured:
|
||||
[31m21|omnirou | [39m[bootstrap] • Antigravity OAuth (ANTIGRAVITY_OAUTH_CLIENT_SECRET) — set in .env or /Users/work/.omniroute/server.env
|
||||
[31m21|omnirou | [39m[bootstrap] • Qoder OAuth (QODER_OAUTH_CLIENT_SECRET) — set in .env or /Users/work/.omniroute/server.env
|
||||
[31m21|omnirou | [39m[bootstrap] These providers will not work until configured.
|
||||
[31m21|omnirou | [39m[bootstrap] ⚠️ INITIAL_PASSWORD is not set — using default 'CHANGEME'. Change it in Settings!
|
||||
[31m21|omnirou | [39m⨯ Another next dev server is already running.
|
||||
[31m21|omnirou | [39m
|
||||
[31m21|omnirou | [39m- Local: http://localhost:3001
|
||||
[31m21|omnirou | [39m- PID: 20496
|
||||
[31m21|omnirou | [39m- Dir: /Users/work/serpentos/Hermes_Omni
|
||||
[31m21|omnirou | [39m- Log: .build/next/dev/logs/next-development.log
|
||||
[31m21|omnirou | [39m
|
||||
[31m21|omnirou | [39mYou can access the existing server at http://localhost:3001,
|
||||
[31m21|omnirou | [39mor run kill 20496 to stop it and start a new one.
|
||||
[31m21|omnirou | [39mnpm notice run omniroute@3.8.50 dev
|
||||
[31m21|omnirou | [39mnpm notice run node --max-old-space-size=8192 scripts/dev/run-next.mjs dev --port 20128
|
||||
[31m21|omnirou | [39m[bootstrap] ℹ️ The following OAuth integrations are not configured:
|
||||
[31m21|omnirou | [39m[bootstrap] • Antigravity OAuth (ANTIGRAVITY_OAUTH_CLIENT_SECRET) — set in .env or /Users/work/.omniroute/server.env
|
||||
[31m21|omnirou | [39m[bootstrap] • Qoder OAuth (QODER_OAUTH_CLIENT_SECRET) — set in .env or /Users/work/.omniroute/server.env
|
||||
[31m21|omnirou | [39m[bootstrap] These providers will not work until configured.
|
||||
[31m21|omnirou | [39m[bootstrap] ⚠️ INITIAL_PASSWORD is not set — using default 'CHANGEME'. Change it in Settings!
|
||||
[31m21|omnirou | [39mnpm notice run omniroute@3.8.50 dev
|
||||
[31m21|omnirou | [39mnpm notice run node --max-old-space-size=8192 scripts/dev/run-next.mjs dev --port 20128
|
||||
[31m21|omnirou | [39m[bootstrap] ℹ️ The following OAuth integrations are not configured:
|
||||
[31m21|omnirou | [39m[bootstrap] • Antigravity OAuth (ANTIGRAVITY_OAUTH_CLIENT_SECRET) — set in .env or /Users/work/.omniroute/server.env
|
||||
[31m21|omnirou | [39m[bootstrap] • Qoder OAuth (QODER_OAUTH_CLIENT_SECRET) — set in .env or /Users/work/.omniroute/server.env
|
||||
[31m21|omnirou | [39m[bootstrap] These providers will not work until configured.
|
||||
[31m21|omnirou | [39m[bootstrap] ⚠️ INITIAL_PASSWORD is not set — using default 'CHANGEME'. Change it in Settings!
|
||||
[31m21|omnirou | [39m⨯ Another next dev server is already running.
|
||||
[31m21|omnirou | [39m
|
||||
[31m21|omnirou | [39m- Local: http://localhost:3001
|
||||
[31m21|omnirou | [39m- PID: 20496
|
||||
[31m21|omnirou | [39m- Dir: /Users/work/serpentos/Hermes_Omni
|
||||
[31m21|omnirou | [39m- Log: .build/next/dev/logs/next-development.log
|
||||
[31m21|omnirou | [39m
|
||||
[31m21|omnirou | [39mYou can access the existing server at http://localhost:3001,
|
||||
[31m21|omnirou | [39mor run kill 20496 to stop it and start a new one.
|
||||
[31m21|omnirou | [39mnpm notice run omniroute@3.8.50 dev
|
||||
[31m21|omnirou | [39mnpm notice run node --max-old-space-size=8192 scripts/dev/run-next.mjs dev --port 20128
|
||||
[31m21|omnirou | [39m[bootstrap] ℹ️ The following OAuth integrations are not configured:
|
||||
[31m21|omnirou | [39m[bootstrap] • Antigravity OAuth (ANTIGRAVITY_OAUTH_CLIENT_SECRET) — set in .env or /Users/work/.omniroute/server.env
|
||||
[31m21|omnirou | [39m[bootstrap] • Qoder OAuth (QODER_OAUTH_CLIENT_SECRET) — set in .env or /Users/work/.omniroute/server.env
|
||||
[31m21|omnirou | [39m[bootstrap] These providers will not work until configured.
|
||||
[31m21|omnirou | [39m[bootstrap] ⚠️ INITIAL_PASSWORD is not set — using default 'CHANGEME'. Change it in Settings!
|
||||
[31m21|omnirou | [39m⨯ Another next dev server is already running.
|
||||
[31m21|omnirou | [39m
|
||||
[31m21|omnirou | [39m- Local: http://localhost:3001
|
||||
[31m21|omnirou | [39m- PID: 20496
|
||||
[31m21|omnirou | [39m- Dir: /Users/work/serpentos/Hermes_Omni
|
||||
[31m21|omnirou | [39m- Log: .build/next/dev/logs/next-development.log
|
||||
[31m21|omnirou | [39m
|
||||
[31m21|omnirou | [39mYou can access the existing server at http://localhost:3001,
|
||||
[31m21|omnirou | [39mor run kill 20496 to stop it and start a new one.
|
||||
[31m21|omnirou | [39mnpm notice run omniroute@3.8.50 dev
|
||||
[31m21|omnirou | [39mnpm notice run node --max-old-space-size=8192 scripts/dev/run-next.mjs dev --port 20128
|
||||
[31m21|omnirou | [39m[bootstrap] ℹ️ The following OAuth integrations are not configured:
|
||||
[31m21|omnirou | [39m[bootstrap] • Antigravity OAuth (ANTIGRAVITY_OAUTH_CLIENT_SECRET) — set in .env or /Users/work/.omniroute/server.env
|
||||
[31m21|omnirou | [39m[bootstrap] • Qoder OAuth (QODER_OAUTH_CLIENT_SECRET) — set in .env or /Users/work/.omniroute/server.env
|
||||
[31m21|omnirou | [39m[bootstrap] These providers will not work until configured.
|
||||
[31m21|omnirou | [39m[bootstrap] ⚠️ INITIAL_PASSWORD is not set — using default 'CHANGEME'. Change it in Settings!
|
||||
[31m21|omnirou | [39m⨯ Another next dev server is already running.
|
||||
[31m21|omnirou | [39m
|
||||
[31m21|omnirou | [39m- Local: http://localhost:3001
|
||||
[31m21|omnirou | [39m- PID: 20496
|
||||
[31m21|omnirou | [39m- Dir: /Users/work/serpentos/Hermes_Omni
|
||||
[31m21|omnirou | [39m- Log: .build/next/dev/logs/next-development.log
|
||||
[31m21|omnirou | [39m
|
||||
[31m21|omnirou | [39mYou can access the existing server at http://localhost:3001,
|
||||
[31m21|omnirou | [39mor run kill 20496 to stop it and start a new one.
|
||||
[31m21|omnirou | [39mnpm notice run omniroute@3.8.50 dev
|
||||
[31m21|omnirou | [39mnpm notice run node --max-old-space-size=8192 scripts/dev/run-next.mjs dev --port 20128
|
||||
[31m21|omnirou | [39m[bootstrap] ℹ️ The following OAuth integrations are not configured:
|
||||
[31m21|omnirou | [39m[bootstrap] • Antigravity OAuth (ANTIGRAVITY_OAUTH_CLIENT_SECRET) — set in .env or /Users/work/.omniroute/server.env
|
||||
[31m21|omnirou | [39m[bootstrap] • Qoder OAuth (QODER_OAUTH_CLIENT_SECRET) — set in .env or /Users/work/.omniroute/server.env
|
||||
[31m21|omnirou | [39m[bootstrap] These providers will not work until configured.
|
||||
[31m21|omnirou | [39m[bootstrap] ⚠️ INITIAL_PASSWORD is not set — using default 'CHANGEME'. Change it in Settings!
|
||||
[31m21|omnirou | [39mnpm notice run omniroute@3.8.50 dev
|
||||
[31m21|omnirou | [39mnpm notice run node --max-old-space-size=8192 scripts/dev/run-next.mjs dev --port 20128
|
||||
[31m21|omnirou | [39m[bootstrap] ℹ️ The following OAuth integrations are not configured:
|
||||
[31m21|omnirou | [39m[bootstrap] • Antigravity OAuth (ANTIGRAVITY_OAUTH_CLIENT_SECRET) — set in .env or /Users/work/.omniroute/server.env
|
||||
[31m21|omnirou | [39m[bootstrap] • Qoder OAuth (QODER_OAUTH_CLIENT_SECRET) — set in .env or /Users/work/.omniroute/server.env
|
||||
[31m21|omnirou | [39m[bootstrap] These providers will not work until configured.
|
||||
[31m21|omnirou | [39m[bootstrap] ⚠️ INITIAL_PASSWORD is not set — using default 'CHANGEME'. Change it in Settings!
|
||||
[31m21|omnirou | [39mnpm notice run omniroute@3.8.50 dev
|
||||
[31m21|omnirou | [39mnpm notice run node --max-old-space-size=8192 scripts/dev/run-next.mjs dev --port 20128
|
||||
[31m21|omnirou | [39m[bootstrap] ℹ️ The following OAuth integrations are not configured:
|
||||
[31m21|omnirou | [39m[bootstrap] • Antigravity OAuth (ANTIGRAVITY_OAUTH_CLIENT_SECRET) — set in .env or /Users/work/.omniroute/server.env
|
||||
[31m21|omnirou | [39m[bootstrap] • Qoder OAuth (QODER_OAUTH_CLIENT_SECRET) — set in .env or /Users/work/.omniroute/server.env
|
||||
[31m21|omnirou | [39m[bootstrap] These providers will not work until configured.
|
||||
[31m21|omnirou | [39m[bootstrap] ⚠️ INITIAL_PASSWORD is not set — using default 'CHANGEME'. Change it in Settings!
|
||||
[31m21|omnirou | [39mnpm notice run omniroute@3.8.50 dev
|
||||
[31m21|omnirou | [39mnpm notice run node --max-old-space-size=8192 scripts/dev/run-next.mjs dev --port 20128
|
||||
[31m21|omnirou | [39m[bootstrap] ℹ️ The following OAuth integrations are not configured:
|
||||
[31m21|omnirou | [39m[bootstrap] • Antigravity OAuth (ANTIGRAVITY_OAUTH_CLIENT_SECRET) — set in .env or /Users/work/.omniroute/server.env
|
||||
[31m21|omnirou | [39m[bootstrap] • Qoder OAuth (QODER_OAUTH_CLIENT_SECRET) — set in .env or /Users/work/.omniroute/server.env
|
||||
[31m21|omnirou | [39m[bootstrap] These providers will not work until configured.
|
||||
[31m21|omnirou | [39m[bootstrap] ⚠️ INITIAL_PASSWORD is not set — using default 'CHANGEME'. Change it in Settings!
|
||||
[31m21|omnirou | [39m[ARENA_ELO_SYNC] Failed to fetch "text" leaderboard: The operation was aborted due to timeout
|
||||
[31m21|omnirou | [39m[ARENA_ELO_SYNC] Failed to fetch "code" leaderboard: The operation was aborted due to timeout
|
||||
[31m21|omnirou | [39m[ARENA_ELO_SYNC] Sync failed: All Arena leaderboard fetches failed: The operation was aborted due to timeout; The operation was aborted due to timeout
|
||||
[31m21|omnirou | [39m[ProxyFetch] Undici dispatcher failed, falling back to native fetch (after retry): TypeError fetch failed code=PROXY_UNREACHABLE | connect ECONNREFUSED 127.0.0.1:3001 code=ECONNREFUSED syscall=connect errno=-61 address=127.0.0.1:3001
|
||||
[31m21|omnirou | [39m[ProxyFetch] native fetch fallback ALSO failed: dispatcher=[TypeError fetch failed code=PROXY_UNREACHABLE | connect ECONNREFUSED 127.0.0.1:3001 code=ECONNREFUSED syscall=connect errno=-61 address=127.0.0.1:3001] native=[TypeError fetch failed | connect ECONNREFUSED 127.0.0.1:3001 code=ECONNREFUSED syscall=connect errno=-61 address=127.0.0.1:3001]
|
||||
[31m21|omnirou | [39m[ModelSync] github (34afe2ad): fetch failed — fetch failed
|
||||
[31m21|omnirou | [39m[ProxyFetch] Undici dispatcher failed, falling back to native fetch (after retry): TypeError fetch failed code=PROXY_UNREACHABLE | connect ECONNREFUSED 127.0.0.1:3001 code=ECONNREFUSED syscall=connect errno=-61 address=127.0.0.1:3001
|
||||
[31m21|omnirou | [39m[ProxyFetch] Undici dispatcher failed, falling back to native fetch (after retry): TypeError fetch failed code=PROXY_UNREACHABLE | connect ECONNREFUSED 127.0.0.1:3001 code=ECONNREFUSED syscall=connect errno=-61 address=127.0.0.1:3001
|
||||
[31m21|omnirou | [39m[ProxyFetch] native fetch fallback ALSO failed: dispatcher=[TypeError fetch failed code=PROXY_UNREACHABLE | connect ECONNREFUSED 127.0.0.1:3001 code=ECONNREFUSED syscall=connect errno=-61 address=127.0.0.1:3001] native=[TypeError fetch failed | connect ECONNREFUSED 127.0.0.1:3001 code=ECONNREFUSED syscall=connect errno=-61 address=127.0.0.1:3001]
|
||||
[31m21|omnirou | [39m[ModelSync] slavakononovtattoo@gmail.com (176734ec): fetch failed — fetch failed
|
||||
[31m21|omnirou | [39m[ProxyFetch] native fetch fallback ALSO failed: dispatcher=[TypeError fetch failed code=PROXY_UNREACHABLE | connect ECONNREFUSED 127.0.0.1:3001 code=ECONNREFUSED syscall=connect errno=-61 address=127.0.0.1:3001] native=[TypeError fetch failed | connect ECONNREFUSED 127.0.0.1:3001 code=ECONNREFUSED syscall=connect errno=-61 address=127.0.0.1:3001]
|
||||
[31m21|omnirou | [39m[ModelSync] Kimi Code (API) (kimi-cod): fetch failed — fetch failed
|
||||
[31m21|omnirou | [39m[ProxyFetch] Undici dispatcher failed, falling back to native fetch (after retry): TypeError fetch failed code=PROXY_UNREACHABLE | connect ECONNREFUSED 127.0.0.1:3001 code=ECONNREFUSED syscall=connect errno=-61 address=127.0.0.1:3001
|
||||
[31m21|omnirou | [39m[ProxyFetch] native fetch fallback ALSO failed: dispatcher=[TypeError fetch failed code=PROXY_UNREACHABLE | connect ECONNREFUSED 127.0.0.1:3001 code=ECONNREFUSED syscall=connect errno=-61 address=127.0.0.1:3001] native=[TypeError fetch failed | connect ECONNREFUSED 127.0.0.1:3001 code=ECONNREFUSED syscall=connect errno=-61 address=127.0.0.1:3001]
|
||||
[31m21|omnirou | [39m[ModelSync] Antigravity (antigrav): fetch failed — fetch failed
|
||||
[31m21|omnirou | [39m[ProxyFetch] Undici dispatcher failed, falling back to native fetch (after retry): TypeError fetch failed code=PROXY_UNREACHABLE | AggregateError code=ECONNREFUSED | ↳ code=ECONNREFUSED syscall=connect address=::1:20128 | ↳ code=ECONNREFUSED syscall=connect address=127.0.0.1:20128
|
||||
[31m21|omnirou | [39m[ProxyFetch] native fetch fallback ALSO failed: dispatcher=[TypeError fetch failed code=PROXY_UNREACHABLE | AggregateError code=ECONNREFUSED | ↳ code=ECONNREFUSED syscall=connect address=::1:20128 | ↳ code=ECONNREFUSED syscall=connect address=127.0.0.1:20128] native=[TypeError fetch failed | AggregateError code=ECONNREFUSED | ↳ code=ECONNREFUSED syscall=connect address=::1:20128 | ↳ code=ECONNREFUSED syscall=connect address=127.0.0.1:20128]
|
||||
[31m21|omnirou | [39m[ProxyFetch] Undici dispatcher failed, falling back to native fetch (after retry): TypeError fetch failed | getaddrinfo ENOTFOUND llamagate.ai code=ENOTFOUND syscall=getaddrinfo errno=-3008
|
||||
[31m21|omnirou | [39m[ProxyFetch] native fetch fallback ALSO failed: dispatcher=[TypeError fetch failed | getaddrinfo ENOTFOUND llamagate.ai code=ENOTFOUND syscall=getaddrinfo errno=-3008] native=[TypeError fetch failed | getaddrinfo ENOTFOUND llamagate.ai code=ENOTFOUND syscall=getaddrinfo errno=-3008]
|
||||
[31m21|omnirou | [39m[ProxyFetch] Undici dispatcher failed, falling back to native fetch (after retry): TypeError fetch failed | getaddrinfo ENOTFOUND llamagate.ai code=ENOTFOUND syscall=getaddrinfo errno=-3008
|
||||
[31m21|omnirou | [39m[ProxyFetch] native fetch fallback ALSO failed: dispatcher=[TypeError fetch failed | getaddrinfo ENOTFOUND llamagate.ai code=ENOTFOUND syscall=getaddrinfo errno=-3008] native=[TypeError fetch failed | getaddrinfo ENOTFOUND llamagate.ai code=ENOTFOUND syscall=getaddrinfo errno=-3008]
|
||||
[31m21|omnirou | [39m[ProxyFetch] Undici dispatcher failed, falling back to native fetch (after retry): TypeError fetch failed | getaddrinfo ENOTFOUND api.galadriel.ai code=ENOTFOUND syscall=getaddrinfo errno=-3008
|
||||
[31m21|omnirou | [39m[ProxyFetch] native fetch fallback ALSO failed: dispatcher=[TypeError fetch failed | getaddrinfo ENOTFOUND api.galadriel.ai code=ENOTFOUND syscall=getaddrinfo errno=-3008] native=[TypeError fetch failed | getaddrinfo ENOTFOUND api.galadriel.ai code=ENOTFOUND syscall=getaddrinfo errno=-3008]
|
||||
[31m21|omnirou | [39m[ProxyFetch] Undici dispatcher failed, falling back to native fetch (after retry): TypeError fetch failed | getaddrinfo ENOTFOUND api.galadriel.ai code=ENOTFOUND syscall=getaddrinfo errno=-3008
|
||||
[31m21|omnirou | [39m[ProxyFetch] native fetch fallback ALSO failed: dispatcher=[TypeError fetch failed | getaddrinfo ENOTFOUND api.galadriel.ai code=ENOTFOUND syscall=getaddrinfo errno=-3008] native=[TypeError fetch failed | getaddrinfo ENOTFOUND api.galadriel.ai code=ENOTFOUND syscall=getaddrinfo errno=-3008]
|
||||
[31m21|omnirou | [39m[ProxyFetch] Undici dispatcher failed, falling back to native fetch (after retry): TypeError fetch failed | getaddrinfo ENOTFOUND api.lambda.ai code=ENOTFOUND syscall=getaddrinfo errno=-3008
|
||||
[31m21|omnirou | [39m[ProxyFetch] native fetch fallback ALSO failed: dispatcher=[TypeError fetch failed | getaddrinfo ENOTFOUND api.lambda.ai code=ENOTFOUND syscall=getaddrinfo errno=-3008] native=[TypeError fetch failed | getaddrinfo ENOTFOUND api.lambda.ai code=ENOTFOUND syscall=getaddrinfo errno=-3008]
|
||||
[31m21|omnirou | [39m[ProxyFetch] Undici dispatcher failed, falling back to native fetch (after retry): TypeError fetch failed | getaddrinfo ENOTFOUND api.lambda.ai code=ENOTFOUND syscall=getaddrinfo errno=-3008
|
||||
[31m21|omnirou | [39m[ProxyFetch] native fetch fallback ALSO failed: dispatcher=[TypeError fetch failed | getaddrinfo ENOTFOUND api.lambda.ai code=ENOTFOUND syscall=getaddrinfo errno=-3008] native=[TypeError fetch failed | getaddrinfo ENOTFOUND api.lambda.ai code=ENOTFOUND syscall=getaddrinfo errno=-3008]
|
||||
[31m21|omnirou | [39m[ProxyFetch] Undici dispatcher failed, falling back to native fetch (after retry): TypeError fetch failed | getaddrinfo ENOTFOUND api.monsterapi.ai code=ENOTFOUND syscall=getaddrinfo errno=-3008
|
||||
[31m21|omnirou | [39m[ProxyFetch] native fetch fallback ALSO failed: dispatcher=[TypeError fetch failed | getaddrinfo ENOTFOUND api.monsterapi.ai code=ENOTFOUND syscall=getaddrinfo errno=-3008] native=[TypeError fetch failed | getaddrinfo ENOTFOUND api.monsterapi.ai code=ENOTFOUND syscall=getaddrinfo errno=-3008]
|
||||
[31m21|omnirou | [39m[ProxyFetch] Undici dispatcher failed, falling back to native fetch (after retry): TypeError fetch failed | getaddrinfo ENOTFOUND api.monsterapi.ai code=ENOTFOUND syscall=getaddrinfo errno=-3008
|
||||
[31m21|omnirou | [39m[ProxyFetch] native fetch fallback ALSO failed: dispatcher=[TypeError fetch failed | getaddrinfo ENOTFOUND api.monsterapi.ai code=ENOTFOUND syscall=getaddrinfo errno=-3008] native=[TypeError fetch failed | getaddrinfo ENOTFOUND api.monsterapi.ai code=ENOTFOUND syscall=getaddrinfo errno=-3008]
|
||||
[31m21|omnirou | [39m[ProxyFetch] Undici dispatcher failed, falling back to native fetch (after retry): TypeError fetch failed | getaddrinfo ENOTFOUND serving.app.predibase.com code=ENOTFOUND syscall=getaddrinfo errno=-3008
|
||||
[31m21|omnirou | [39m[ProxyFetch] native fetch fallback ALSO failed: dispatcher=[TypeError fetch failed | getaddrinfo ENOTFOUND serving.app.predibase.com code=ENOTFOUND syscall=getaddrinfo errno=-3008] native=[TypeError fetch failed | getaddrinfo ENOTFOUND serving.app.predibase.com code=ENOTFOUND syscall=getaddrinfo errno=-3008]
|
||||
[31m21|omnirou | [39m[ProxyFetch] Undici dispatcher failed, falling back to native fetch (after retry): TypeError fetch failed | getaddrinfo ENOTFOUND serving.app.predibase.com code=ENOTFOUND syscall=getaddrinfo errno=-3008
|
||||
[31m21|omnirou | [39m[ProxyFetch] native fetch fallback ALSO failed: dispatcher=[TypeError fetch failed | getaddrinfo ENOTFOUND serving.app.predibase.com code=ENOTFOUND syscall=getaddrinfo errno=-3008] native=[TypeError fetch failed | getaddrinfo ENOTFOUND serving.app.predibase.com code=ENOTFOUND syscall=getaddrinfo errno=-3008]
|
||||
[31m21|omnirou | [39m[ProxyFetch] Undici dispatcher failed, falling back to native fetch (after retry): TypeError fetch failed | self-signed certificate in certificate chain code=SELF_SIGNED_CERT_IN_CHAIN
|
||||
[31m21|omnirou | [39m[ProxyFetch] native fetch fallback ALSO failed: dispatcher=[TypeError fetch failed | self-signed certificate in certificate chain code=SELF_SIGNED_CERT_IN_CHAIN] native=[TypeError fetch failed | self-signed certificate in certificate chain code=SELF_SIGNED_CERT_IN_CHAIN]
|
||||
[31m21|omnirou | [39mnpm notice run omniroute@3.8.50 dev
|
||||
[31m21|omnirou | [39mnpm notice run node --max-old-space-size=8192 scripts/dev/run-next.mjs dev --port 20128
|
||||
[31m21|omnirou | [39m[bootstrap] ℹ️ The following OAuth integrations are not configured:
|
||||
[31m21|omnirou | [39m[bootstrap] • Antigravity OAuth (ANTIGRAVITY_OAUTH_CLIENT_SECRET) — set in .env or /Users/work/.omniroute/server.env
|
||||
[31m21|omnirou | [39m[bootstrap] • Qoder OAuth (QODER_OAUTH_CLIENT_SECRET) — set in .env or /Users/work/.omniroute/server.env
|
||||
[31m21|omnirou | [39m[bootstrap] These providers will not work until configured.
|
||||
[31m21|omnirou | [39m[bootstrap] ⚠️ INITIAL_PASSWORD is not set — using default 'CHANGEME'. Change it in Settings!
|
||||
|
||||
[90m/Users/work/.pm2/logs/omniroute-out.log last 200 lines:[39m
|
||||
[32m21|omnirou | [39m[STARTUP] Provider limits sync scheduler started
|
||||
[32m21|omnirou | [39m[INFO] [QuotaAutoPing] scheduler started
|
||||
[32m21|omnirou | [39m[STARTUP] Quota auto-ping scheduler started (opt-in, no-op until enabled)
|
||||
[32m21|omnirou | [39m[ModelSync] Scheduler started — interval: 24h
|
||||
[32m21|omnirou | [39m[STARTUP] Cloud/model sync background bootstrap initialized
|
||||
[32m21|omnirou | [39m[BATCH] Initializing batch processor polling...
|
||||
[32m21|omnirou | [39m[STARTUP] Batch processor started
|
||||
[32m21|omnirou | [39m[STARTUP] Runtime settings hydrated: payloadRules, modelAliases, backgroundDegradation, cliCompatProviders, cacheControl, usageTracking, healthCheckLogs, thoughtSignature, modelsDevSync, corsOrigins, ccBridgeTransforms, systemTransforms, authzBypass, bannedSignals
|
||||
[32m21|omnirou | [39m[STARTUP] Model alias seed: applied=0, skipped=2, removed=0, failed=0
|
||||
[32m21|omnirou | [39m[HOT_RELOAD] Runtime config hot-reload started (poll=5000ms, fsWatch=on)
|
||||
[32m21|omnirou | [39m[proxyLogger] Loaded 200 proxy logs from SQLite
|
||||
[32m21|omnirou | [39m[CredentialHealth] Starting credential health check (initial delay 30s, interval 300s)
|
||||
[32m21|omnirou | [39m[STARTUP] Credential health scheduler started
|
||||
[32m21|omnirou | [39m[COMPLIANCE] Audit log table initialized
|
||||
[32m21|omnirou | [39m[STARTUP] Scheduled VACUUM initialized (#4437)
|
||||
[32m21|omnirou | [39m[AutoRefreshDaemon] Started — checking 0 credentials every 900s
|
||||
[32m21|omnirou | [39m[ARENA_ELO_SYNC] Starting periodic sync every 86400s
|
||||
[32m21|omnirou | [39m[PRICING_SYNC] Disabled (set PRICING_SYNC_ENABLED=true to enable)
|
||||
[32m21|omnirou | [39m[STARTUP] Arena ELO sync initialized
|
||||
[32m21|omnirou | [39m[ConnectionRecovery] Starting proactive cooldown recovery (tick every 60s)
|
||||
[32m21|omnirou | [39m[LiveWS] Seeded latest compression run from analytics: a621d3da-eee5-4f34-8515-309d21dc1415
|
||||
[32m21|omnirou | [39m[STARTUP] Live dashboard WebSocket daemon bootstrap invoked
|
||||
[32m21|omnirou | [39m[STARTUP] Embedded services bootstrap complete
|
||||
[32m21|omnirou | [39m[MODELS_DEV] Disabled (enable via Settings > AI)
|
||||
[32m21|omnirou | [39m[MODEL] Treating "deepseek-ai/deepseek-v4-flash" as an exact model id
|
||||
[32m21|omnirou | [39m[MODEL] Treating "deepseek-ai/deepseek-v4-pro" as an exact model id
|
||||
[32m21|omnirou | [39m[MODEL] Treating "deepseek-ai/deepseek-v4-pro" as an exact model id
|
||||
[32m21|omnirou | [39m[MODEL] Treating "deepseek-ai/deepseek-v4-flash" as an exact model id
|
||||
[32m21|omnirou | [39m[MODEL] Treating "deepseek-ai/deepseek-v4-pro" as an exact model id
|
||||
[32m21|omnirou | [39m[MODEL] Treating "deepseek-ai/deepseek-v4-pro" as an exact model id
|
||||
[32m21|omnirou | [39m[MODEL] Treating "deepseek-ai/deepseek-v4-flash" as an exact model id
|
||||
[32m21|omnirou | [39m[MODEL] Treating "deepseek-ai/deepseek-v4-pro" as an exact model id
|
||||
[32m21|omnirou | [39m[MODEL] Treating "deepseek-ai/deepseek-v4-pro" as an exact model id
|
||||
[32m21|omnirou | [39m[MODEL] Treating "deepseek-ai/deepseek-v4-flash" as an exact model id
|
||||
[32m21|omnirou | [39m[MODEL] Treating "deepseek-ai/deepseek-v4-pro" as an exact model id
|
||||
[32m21|omnirou | [39m[MODEL] Treating "deepseek-ai/deepseek-v4-pro" as an exact model id
|
||||
[32m21|omnirou | [39m[MODEL] Treating "deepseek-ai/deepseek-v4-pro" as an exact model id
|
||||
[32m21|omnirou | [39m[MODEL] Treating "openai/gpt-oss-120b" as an exact model id
|
||||
[32m21|omnirou | [39m[MODEL] Treating "deepseek-ai/deepseek-v4-pro" as an exact model id
|
||||
[32m21|omnirou | [39m[MODEL] Treating "openai/gpt-oss-120b" as an exact model id
|
||||
[32m21|omnirou | [39m[MODEL] Treating "deepseek-ai/deepseek-v4-pro" as an exact model id
|
||||
[32m21|omnirou | [39m[MODEL] Treating "openai/gpt-oss-120b" as an exact model id
|
||||
[32m21|omnirou | [39m[MODEL] Treating "deepseek-ai/deepseek-v4-pro" as an exact model id
|
||||
[32m21|omnirou | [39m[MODEL] Treating "openai/gpt-oss-120b" as an exact model id
|
||||
[32m21|omnirou | [39m[MODEL] Treating "qwen/qwen3.6-27b" as an exact model id
|
||||
[32m21|omnirou | [39m[MODEL] Treating "qwen/qwen3.6-27b" as an exact model id
|
||||
[32m21|omnirou | [39m[MODEL] Treating "qwen/qwen3.6-27b" as an exact model id
|
||||
[32m21|omnirou | [39m[MODEL] Treating "qwen/qwen3.6-27b" as an exact model id
|
||||
[32m21|omnirou | [39m[MODEL] Treating "qwen/qwen3.6-27b" as an exact model id
|
||||
[32m21|omnirou | [39m[MODEL] Treating "moonshotai/Kimi-K2.7-Code" as an exact model id
|
||||
[32m21|omnirou | [39m[MODEL] Treating "qwen/qwen3.6-27b" as an exact model id
|
||||
[32m21|omnirou | [39m[MODEL] Treating "moonshotai/Kimi-K2.7-Code" as an exact model id
|
||||
[32m21|omnirou | [39m[MODEL] Treating "qwen/qwen3.6-27b" as an exact model id
|
||||
[32m21|omnirou | [39m[MODEL] Treating "moonshotai/Kimi-K2.7-Code" as an exact model id
|
||||
[32m21|omnirou | [39m[MODEL] Treating "qwen/qwen3.6-27b" as an exact model id
|
||||
[32m21|omnirou | [39m[MODEL] Treating "moonshotai/Kimi-K2.7-Code" as an exact model id
|
||||
[32m21|omnirou | [39m[STARTUP] Model catalog cache warmed
|
||||
[32m21|omnirou | [39m[STARTUP] OpenRouter model catalog cache warmed
|
||||
[32m21|omnirou | [39m[EmbedWsProxy] Listening on 127.0.0.1:20131
|
||||
[32m21|omnirou | [39m[LiveWS] Dashboard WebSocket server listening on 127.0.0.1:20132
|
||||
[32m21|omnirou | [39m[ModelSync] Starting model sync cycle — 4 connection(s)
|
||||
[32m21|omnirou | [39m[HealthCheck] github/34afe2ad-6d24-4ccd-9747-ac430ba73aa4 has no refresh token but has a GitHub access token; keeping connection active
|
||||
[32m21|omnirou | [39m[CredentialHealth] Testing 250/250 connections...
|
||||
[32m21|omnirou | [39m[CredentialHealth] ❌ openai-compatible/0a53f78b-a2ad-4e9f-9668-e1c1f2ac951d — Provider test not supported [11ms] (failure #1, next check in 600s)
|
||||
[32m21|omnirou | [39m[CredentialHealth] ❌ qwen/f5626345-947f-46f8-930a-5dabd6a68331 — Provider test not supported [13ms] (failure #1, next check in 600s)
|
||||
[32m21|omnirou | [39m[Next] dev server listening on http://0.0.0.0:3001 (turbopack)
|
||||
[32m21|omnirou | [39m[ModelSync] Cycle complete: 0/4 synced in 189ms
|
||||
[32m21|omnirou | [39m[CredentialHealth] ❌ puter/puter-free — Invalid API key [300ms] (failure #1, next check in 600s)
|
||||
[32m21|omnirou | [39m[CredentialHealth] ❌ longcat/bfb70bc9-4e06-4d63-8283-630228e900e8 — Invalid API key [499ms] (failure #1, next check in 600s)
|
||||
[32m21|omnirou | [39m[CredentialHealth] ❌ qoder/qoder-free — Local CLI runtime is not installed [2342ms] (failure #1, next check in 600s)
|
||||
[32m21|omnirou | [39m[CredentialHealth] ❌ huggingface/b684540c-e84f-4591-a435-71a7620c1999 — Cannot convert argument to a ByteString because the character at index 41 has a value of 8232 which is greater than 255. [2479ms] (failure #1, next check in 600s)
|
||||
[32m21|omnirou | [39m[CredentialHealth] ❌ xai-oauth/809a7805-67c4-4085-9195-6c0708189eaa — API returned 402 [2575ms] (failure #1, next check in 600s)
|
||||
[32m21|omnirou | [39m[CredentialHealth] ❌ dgrid/1454ba3b-53d9-4d44-a322-910f62f19ac7 — Invalid API key [298ms] (failure #1, next check in 600s)
|
||||
[32m21|omnirou | [39m[CredentialHealth] ❌ cheaperinference/80519c86-cc63-4ad6-981a-de0279e49d16 — Invalid API key [435ms] (failure #1, next check in 600s)
|
||||
[32m21|omnirou | [39m[CredentialHealth] ❌ command-code/d63168d8-5a22-4be8-a1e2-b57a18683cce — Invalid API key [579ms] (failure #1, next check in 600s)
|
||||
[32m21|omnirou | [39m[CredentialHealth] ❌ agentrouter/d1556537-17c8-44f1-85ab-3a1daae0dfab — Invalid API key [1356ms] (failure #1, next check in 600s)
|
||||
[32m21|omnirou | [39m[CredentialHealth] ❌ orcarouter/3b98bf6e-dead-4807-8146-c4a67aa9b23b — Invalid API key [490ms] (failure #1, next check in 600s)
|
||||
[32m21|omnirou | [39m[05:02:57.823] [32mINFO[39m: [36m[sse] Account 7bd8a34d error cleared (CAS)[39m
|
||||
[32m21|omnirou | [39m [35mmodule[39m: "sse"
|
||||
[32m21|omnirou | [39m [35mtag[39m: "AUTH"
|
||||
[32m21|omnirou | [39m[CredentialHealth] ❌ wafer/b02994c7-3bb6-4753-ae11-d85dc5e2a0e9 — Invalid API key [1410ms] (failure #1, next check in 600s)
|
||||
[32m21|omnirou | [39m[CredentialHealth] ❌ freetheai/b6fa0b94-088c-4693-9221-a96f795eb7d4 — Invalid API key [375ms] (failure #1, next check in 600s)
|
||||
[32m21|omnirou | [39m[CredentialHealth] ❌ uncloseai/79e43f97-1192-47aa-8f52-99ea5ed0300d — Provider unavailable (502) [1383ms] (failure #1, next check in 600s)
|
||||
[32m21|omnirou | [39m[CredentialHealth] ❌ vercel-ai-gateway/2b8023d4-82ce-4684-8fe8-c716d361fed8 — Invalid API key [174ms] (failure #1, next check in 600s)
|
||||
[32m21|omnirou | [39m[CredentialHealth] ❌ llamagate/24cf76c7-4894-47ff-af6a-7e2af4720d3a — fetch failed [299ms] (failure #1, next check in 600s)
|
||||
[32m21|omnirou | [39m[CredentialHealth] ❌ gitlawb-gmi/e18cf4cd-fa35-453e-8c8d-2741f6c1eaaf — Invalid API key [321ms] (failure #1, next check in 600s)
|
||||
[32m21|omnirou | [39m[CredentialHealth] ❌ gitlawb/243a74a5-f41b-44b1-bed6-47edc1b9544b — Invalid API key [447ms] (failure #1, next check in 600s)
|
||||
[32m21|omnirou | [39m[CredentialHealth] ❌ piapi/7f56fc63-8257-4998-85e4-5032d7d9a66b — Provider test not supported [28ms] (failure #1, next check in 600s)
|
||||
[32m21|omnirou | [39m[CredentialHealth] ❌ getgoapi/1406f87c-8ddf-44bb-a0fa-cc04b6128306 — Provider test not supported [23ms] (failure #1, next check in 600s)
|
||||
[32m21|omnirou | [39m[CredentialHealth] ❌ laozhang/630cd8b8-8074-49f8-b382-d510ebc9d5e7 — Provider test not supported [24ms] (failure #1, next check in 600s)
|
||||
[32m21|omnirou | [39m[CredentialHealth] ❌ thebai/05fd1dfc-faab-45ce-b9d6-3be9a2ff4276 — Provider test not supported [24ms] (failure #1, next check in 600s)
|
||||
[32m21|omnirou | [39m[CredentialHealth] ❌ poe/d9986d2a-fd90-41c6-8f5e-37a03943fd4c — Invalid API key [332ms] (failure #1, next check in 600s)
|
||||
[32m21|omnirou | [39m[CredentialHealth] ❌ bai/c662bf62-720d-4acd-8d27-913c7e63dccc — Invalid API key [414ms] (failure #1, next check in 600s)
|
||||
[32m21|omnirou | [39m[CredentialHealth] ❌ empower/45ad5813-1da8-4e92-9ca6-9696da0ecd57 — Provider test not supported [34ms] (failure #1, next check in 600s)
|
||||
[32m21|omnirou | [39m[CredentialHealth] ❌ fenayai/486b463e-cbf4-464d-bf18-a2845bd88f4d — Provider test not supported [35ms] (failure #1, next check in 600s)
|
||||
[32m21|omnirou | [39m[CredentialHealth] ❌ factory/aaf1a527-f9af-494f-b0f2-c81ff6605f62 — Provider validation endpoint not supported [405ms] (failure #1, next check in 600s)
|
||||
[32m21|omnirou | [39m[05:03:04.389] [32mINFO[39m: [36m[sse] Account 809a7805 error cleared (CAS)[39m
|
||||
[32m21|omnirou | [39m [35mmodule[39m: "sse"
|
||||
[32m21|omnirou | [39m [35mtag[39m: "AUTH"
|
||||
[32m21|omnirou | [39m[CredentialHealth] ❌ bluesminds/e664de41-5bca-489b-bb85-46e6726b98e0 — Invalid API key [233ms] (failure #1, next check in 600s)
|
||||
[32m21|omnirou | [39m[CredentialHealth] ❌ chutes/add9d1d1-3f43-44c7-b8a5-2bd3bb939203 — Invalid API key [355ms] (failure #1, next check in 600s)
|
||||
[32m21|omnirou | [39m[CredentialHealth] ❌ dit/d59eb062-3f1b-4fa0-a3cc-e275aba9e386 — Invalid API key [795ms] (failure #1, next check in 600s)
|
||||
[32m21|omnirou | [39m[CredentialHealth] ❌ sumopod/f3649287-8b58-41ab-a5c3-02674adbf157 — Invalid API key [1465ms] (failure #1, next check in 600s)
|
||||
[32m21|omnirou | [39m[CredentialHealth] ❌ x5lab/12522685-26e7-4799-a8d3-13b95f1d71e7 — Provider unavailable (530) [317ms] (failure #1, next check in 600s)
|
||||
[32m21|omnirou | [39m[CredentialHealth] ❌ openadapter/2be7647a-1211-4154-8cf1-df1575bb0c23 — Invalid API key [534ms] (failure #1, next check in 600s)
|
||||
[32m21|omnirou | [39m[CredentialHealth] ❌ tokenrouter/62848fc3-351d-4d7b-8b30-50e1adecbfd0 — Invalid API key [807ms] (failure #1, next check in 600s)
|
||||
[32m21|omnirou | [39m[CredentialHealth] ❌ chenzk/cd8f98c9-8dd9-476a-bf9b-9dc3ce0ece72 — Invalid API key [2379ms] (failure #1, next check in 600s)
|
||||
[32m21|omnirou | [39m[CredentialHealth] ❌ aion/97948c8e-564e-470c-abdf-b744c363162b — Invalid API key [330ms] (failure #1, next check in 600s)
|
||||
[32m21|omnirou | [39m[ProxyHealth] Starting proxy health scheduler (interval: 600000ms)
|
||||
[32m21|omnirou | [39m[CredentialHealth] ❌ nara/422b54f9-f654-40fb-906d-fff9b0de72de — Invalid API key [654ms] (failure #1, next check in 600s)
|
||||
[32m21|omnirou | [39m[CredentialHealth] ❌ reka/c30f7b6b-ad2e-46b6-90c3-21926e6c3924 — Invalid API key [153ms] (failure #1, next check in 600s)
|
||||
[32m21|omnirou | [39m[CredentialHealth] ❌ anthropic/ee916845-395a-4184-9b07-9c26f969aaa1 — Invalid API key [477ms] (failure #1, next check in 600s)
|
||||
[32m21|omnirou | [39m[CredentialHealth] ❌ mistral/bec0969f-65ae-41dc-be66-43d36dc8897b — Invalid API key [281ms] (failure #1, next check in 600s)
|
||||
[32m21|omnirou | [39m[CredentialHealth] ❌ cohere/763467f5-1a9a-4ae9-a310-a952387d8050 — Invalid API key [376ms] (failure #1, next check in 600s)
|
||||
[32m21|omnirou | [39m[CredentialHealth] ❌ perplexity/1ce7f3fc-b063-43ff-93e3-2ce96e4de76c — Invalid API key [410ms] (failure #1, next check in 600s)
|
||||
[32m21|omnirou | [39m[CredentialHealth] ❌ galadriel/99886394-bd42-4cf0-b59f-ea73df3f2895 — fetch failed [424ms] (failure #1, next check in 600s)
|
||||
[32m21|omnirou | [39m[CredentialHealth] ❌ morph/ad1d2458-2dd3-4f9a-b653-d9def966be1b — Invalid API key [90ms] (failure #1, next check in 600s)
|
||||
[32m21|omnirou | [39m[CredentialHealth] ❌ meta-llama/27504e6f-2422-4eac-8265-6282a7b31910 — Invalid API key [135ms] (failure #1, next check in 600s)
|
||||
[32m21|omnirou | [39m[CredentialHealth] ❌ nous-research/867d96b1-6466-416a-8d77-7a4d61bf8823 — Invalid API key [311ms] (failure #1, next check in 600s)
|
||||
[32m21|omnirou | [39m[CredentialHealth] ❌ maritalk/bf819d44-a782-4947-8051-aa5a8aeb3304 — Invalid API key [766ms] (failure #1, next check in 600s)
|
||||
[32m21|omnirou | [39m[CredentialHealth] ❌ upstage/d75885b8-e055-48be-afcf-9d09b683543f — Invalid API key [882ms] (failure #1, next check in 600s)
|
||||
[32m21|omnirou | [39m[CredentialHealth] ❌ codestral/81d87255-39e1-4085-8bb7-de1916484c18 — Invalid API key [298ms] (failure #1, next check in 600s)
|
||||
[32m21|omnirou | [39m[CredentialHealth] ❌ writer/42ffd640-20a0-4f9a-8bf2-06a97c359771 — Invalid API key [410ms] (failure #1, next check in 600s)
|
||||
[32m21|omnirou | [39m[CredentialHealth] ❌ liquid/916bc15c-fb8c-4763-b264-5f7794d63f52 — Invalid API key [473ms] (failure #1, next check in 600s)
|
||||
[32m21|omnirou | [39m[CredentialHealth] ❌ fireworks/56d3d5d6-9ea4-4cff-aec5-5b285f6a6e42 — Invalid API key [530ms] (failure #1, next check in 600s)
|
||||
[32m21|omnirou | [39m[CredentialHealth] ❌ arcee-ai/b1a6a4c1-a859-42ec-82f5-62786c1d790e — Provider test not supported [25ms] (failure #1, next check in 600s)
|
||||
[32m21|omnirou | [39m[CredentialHealth] ❌ siliconflow/223f9fd1-0d0b-4306-9487-02ee2a4efccf — Invalid API key [523ms] (failure #1, next check in 600s)
|
||||
[32m21|omnirou | [39m[CredentialHealth] ❌ nube/d9ddeb1b-ebc5-4e92-ac6b-5030011e114c — Invalid API key [685ms] (failure #1, next check in 600s)
|
||||
[32m21|omnirou | [39m[CredentialHealth] ❌ hyperbolic/1acffcad-79bb-4309-adcb-93c7739942c0 — Invalid API key [800ms] (failure #1, next check in 600s)
|
||||
[32m21|omnirou | [39m[CredentialHealth] ❌ openvecta/664194e7-da9d-4884-9112-286782dc1463 — Invalid API key [309ms] (failure #1, next check in 600s)
|
||||
[32m21|omnirou | [39m[CredentialHealth] ❌ nebius/b416aaff-ad3e-4531-b6b8-71f5c97c671e — Invalid API key [353ms] (failure #1, next check in 600s)
|
||||
[32m21|omnirou | [39m[CredentialHealth] ❌ deepinfra/f6307c55-52ac-424b-b4fd-99ecf43d68dc — Invalid API key [701ms] (failure #1, next check in 600s)
|
||||
[32m21|omnirou | [39m[CredentialHealth] ❌ nscale/305c1ad6-1214-42bd-8fd5-6290103c7f6e — Invalid API key [275ms] (failure #1, next check in 600s)
|
||||
[32m21|omnirou | [39m[CredentialHealth] ❌ lambda-ai/02e9af45-1152-4c03-8dce-c475db3952e2 — fetch failed [307ms] (failure #1, next check in 600s)
|
||||
[32m21|omnirou | [39m[CredentialHealth] ❌ baseten/01b22f14-f3e4-4c7e-9a72-8cb515e9a94c — Invalid API key [340ms] (failure #1, next check in 600s)
|
||||
[32m21|omnirou | [39m[CredentialHealth] ❌ featherless-ai/ad32b81a-0fe4-4014-95f6-6f9c42db0a46 — Invalid API key [787ms] (failure #1, next check in 600s)
|
||||
[32m21|omnirou | [39m[CredentialHealth] ❌ publicai/85befdb8-4a2b-45f8-b8d3-24c978fbada6 — Invalid API key [271ms] (failure #1, next check in 600s)
|
||||
[32m21|omnirou | [39m[CredentialHealth] ❌ inference-net/a11e8b2e-55e4-4b4e-b378-36f118100160 — Invalid API key [418ms] (failure #1, next check in 600s)
|
||||
[32m21|omnirou | [39m[CredentialHealth] ❌ wandb/d4732e05-ee59-45b3-b4a1-0c4d00a0807a — Invalid API key [522ms] (failure #1, next check in 600s)
|
||||
[32m21|omnirou | [39m[CredentialHealth] ❌ monsterapi/b54896e1-acf7-4100-9e18-f45b2456acdf — fetch failed [647ms] (failure #1, next check in 600s)
|
||||
[32m21|omnirou | [39m[CredentialHealth] ❌ bytez/1ba3efe1-06e5-48ce-93bf-9dced73dc5ca — Invalid API key [299ms] (failure #1, next check in 600s)
|
||||
[32m21|omnirou | [39m[CredentialHealth] ❌ azure-ai/92b248f6-87b9-479b-9b8b-e52dade0cf40 — Invalid API key [382ms] (failure #1, next check in 600s)
|
||||
[32m21|omnirou | [39m[CredentialHealth] ❌ predibase/3dcf4649-7c03-4fdb-88b2-1bac201fb63a — fetch failed [420ms] (failure #1, next check in 600s)
|
||||
[32m21|omnirou | [39m[CredentialHealth] ❌ bedrock/2511a80c-a6a5-4acc-a25e-3a0439b401ce — Invalid API key [524ms] (failure #1, next check in 600s)
|
||||
[32m21|omnirou | [39m[CredentialHealth] ❌ byteplus/d35eed01-d625-4011-ad2d-4eeca7a39d3f — Invalid API key [746ms] (failure #1, next check in 600s)
|
||||
[32m21|omnirou | [39m[CredentialHealth] ❌ azure-openai/82265a58-9a51-4c9a-9298-d053e64cba05 — Missing base URL [34ms] (failure #1, next check in 600s)
|
||||
[32m21|omnirou | [39m[CredentialHealth] ❌ digitalocean/74fdfca2-e1a6-49c3-8254-a652103f22a7 — Invalid API key [316ms] (failure #1, next check in 600s)
|
||||
[32m21|omnirou | [39m[CredentialHealth] ❌ watsonx/42e05ebc-d134-47ea-ad61-9f20bf324741 — Invalid API key [819ms] (failure #1, next check in 600s)
|
||||
[32m21|omnirou | [39mTOKEN Antigravity refreshed
|
||||
[32m21|omnirou | [39m[CredentialHealth] ❌ oci/0300cc22-79f8-4ef0-8c9f-0340121c1c34 — Invalid API key [1342ms] (failure #1, next check in 600s)
|
||||
[32m21|omnirou | [39m[CredentialHealth] ❌ modal/a5d5b559-97a6-496d-9fe4-a9a29bd96b8f — Invalid outbound URL: [93ms] (failure #1, next check in 600s)
|
||||
[32m21|omnirou | [39m[CredentialHealth] ❌ scaleway/6ef89557-85c0-4175-af68-6e76ef41c8a0 — Invalid API key [237ms] (failure #1, next check in 600s)
|
||||
[32m21|omnirou | [39m[CredentialHealth] ❌ ovhcloud/35d1b82e-4643-43ef-8098-9fd3f5eb8a8b — Invalid API key [269ms] (failure #1, next check in 600s)
|
||||
[32m21|omnirou | [39m[CredentialHealth] ❌ databricks/a16ac17b-4de3-416e-aa5d-71c2fad52d18 — Missing base URL [40ms] (failure #1, next check in 600s)
|
||||
[32m21|omnirou | [39mTOKEN Antigravity projectId discovered during refresh: polar-weaver-2tclv
|
||||
[32m21|omnirou | [39m[CredentialHealth] ❌ clarifai/aa71f371-f781-40af-946c-99155c4c3018 — Invalid API key [692ms] (failure #1, next check in 600s)
|
||||
[32m21|omnirou | [39m[CredentialHealth] ❌ qianfan/bbf3bde6-93ff-4b3a-acb5-a097111fb5ea — Invalid API key [1093ms] (failure #1, next check in 600s)
|
||||
[32m21|omnirou | [39m[CredentialHealth] ❌ heroku/91c06d49-073c-49d9-b64f-f59bd8330023 — Missing base URL [28ms] (failure #1, next check in 600s)
|
||||
[32m21|omnirou | [39m[CredentialHealth] ❌ snowflake/94a20d62-fa07-41ba-bdad-48d076d0b8e9 — Missing base URL [29ms] (failure #1, next check in 600s)
|
||||
[32m21|omnirou | [39m[CredentialHealth] ❌ bailian-coding-plan/561f427a-1df7-46cf-899e-9ed61345fe5b — Invalid API key [858ms] (failure #1, next check in 600s)
|
||||
[32m21|omnirou | [39m[CredentialHealth] ❌ glm/37a8e485-3177-46ca-9fd4-53408634e872 — Invalid API key [1003ms] (failure #1, next check in 600s)
|
||||
[32m21|omnirou | [39m[CredentialHealth] ❌ glmt/ca7a155c-988d-43ba-971c-571bb0d679c1 — Invalid API key [1035ms] (failure #1, next check in 600s)
|
||||
[32m21|omnirou | [39m[CredentialHealth] ❌ glm-cn/d4830be9-b2ea-462a-8067-c35f59aab73f — Invalid API key [288ms] (failure #1, next check in 600s)
|
||||
[32m21|omnirou | [39m[CredentialHealth] ❌ qwen-cloud/c22ca10b-3907-4e6f-b9f7-53c0850bb313 — Invalid API key [686ms] (failure #1, next check in 600s)
|
||||
[32m21|omnirou | [39m[CredentialHealth] ❌ qwen-cloud-token-plan/fc29d826-8de5-4266-9677-a663fa709cc8 — Invalid API key [722ms] (failure #1, next check in 600s)
|
||||
[32m21|omnirou | [39mTOKEN Antigravity refreshed
|
||||
[32m21|omnirou | [39m[CredentialHealth] ❌ zai/fbff3609-132c-492a-9c50-41a6603aa9b7 — Invalid API key [1539ms] (failure #1, next check in 600s)
|
||||
[32m21|omnirou | [39mTOKEN Antigravity projectId discovered during refresh: isometric-tendril-lfb6s
|
||||
[32m21|omnirou | [39m[CredentialHealth] ❌ minimax-cn/69617fca-c974-41d2-b2a4-5d2f1b849cf9 — Invalid API key [1922ms] (failure #1, next check in 600s)
|
||||
[32m21|omnirou | [39m[CredentialHealth] ❌ kimi/62a54a22-c074-4007-9b7b-9784b2a7cb37 — Invalid API key [520ms] (failure #1, next check in 600s)
|
||||
[32m21|omnirou | [39m[CredentialHealth] ❌ minimax/6e44c652-6ed0-44d2-9c61-c28289284b42 — Invalid API key [660ms] (failure #1, next check in 600s)
|
||||
[32m21|omnirou | [39m[CredentialHealth] ❌ alibaba/db532af8-08a3-4589-bef3-30c8350c2b8d — Invalid API key [882ms] (failure #1, next check in 600s)
|
||||
[32m21|omnirou | [39m[CredentialHealth] ❌ alibaba-cn/f20ca3c9-fe7f-406c-af34-6d388cc72e24 — Invalid API key [1061ms] (failure #1, next check in 600s)
|
||||
[32m21|omnirou | [39m[CredentialHealth] ❌ volcengine/aa953628-bcbd-40ea-a395-acbc7ddca48f — Invalid API key [2233ms] (failure #1, next check in 600s)
|
||||
[32m21|omnirou | [39m[CredentialHealth] ❌ moonshot/994f4192-a991-4996-9bfa-7bb6a7f583f2 — Invalid API key [527ms] (failure #1, next check in 600s)
|
||||
[32m21|omnirou | [39mTOKEN Antigravity refreshed
|
||||
[32m21|omnirou | [39m[CredentialHealth] ❌ xiaomi-mimo-token-plan/67977290-9d6a-4d4c-adc0-821b481829f3 — Invalid API key [927ms] (failure #1, next check in 600s)
|
||||
[32m21|omnirou | [39m[CredentialHealth] ❌ yi/beb4c757-62f7-45d2-a646-c5d298c158aa — Invalid API key [1326ms] (failure #1, next check in 600s)
|
||||
[32m21|omnirou | [39m[CredentialHealth] ❌ tencent/9cd0617a-4971-46fb-9d6d-a0f9e19d5f97 — Invalid API key [1873ms] (failure #1, next check in 600s)
|
||||
[32m21|omnirou | [39mTOKEN Antigravity projectId discovered during refresh: grand-vigil-qmvz5
|
||||
[32m21|omnirou | [39mTOKEN Antigravity refreshed
|
||||
[32m21|omnirou | [39m[CredentialHealth] ❌ baidu/d5b875f1-6395-4430-bac7-b5120304d2c8 — Invalid API key [5878ms] (failure #1, next check in 600s)
|
||||
[32m21|omnirou | [39m[CredentialHealth] ❌ xiaomi-mimo/7bd8a34d-45aa-4159-9c45-d028f4067310 — Invalid API key [286ms] (failure #1, next check in 600s)
|
||||
[32m21|omnirou | [39mTOKEN Antigravity projectId discovered during refresh: isometric-tendril-lfb6s
|
||||
[32m21|omnirou | [39m[CredentialHealth] ❌ gigachat/318c3bfc-ee0c-4f74-82b8-d55e28372837 — fetch failed [796ms] (failure #1, next check in 600s)
|
||||
[32m21|omnirou | [39m[CredentialHealth] ❌ coze/0d817a49-5dd8-4d14-b0c9-49b60216a8e0 — Provider validation endpoint not supported [950ms] (failure #1, next check in 600s)
|
||||
[32m21|omnirou | [39m[CredentialHealth] ❌ doubao/0bbd928d-6d57-404f-8c69-47e6f53af675 — Invalid API key [979ms] (failure #1, next check in 600s)
|
||||
[32m21|omnirou | [39m[CredentialHealth] ❌ stepfun/930f3cf5-16b6-4584-ae7d-5bff7f6dc9c1 — Invalid API key [1211ms] (failure #1, next check in 600s)
|
||||
[32m21|omnirou | [39m
|
||||
[32m21|omnirou | [39m[Shutdown] Received SIGINT. Draining 0 request(s)...
|
||||
[32m21|omnirou | [39m[Shutdown] All in-flight requests drained.
|
||||
[32m21|omnirou | [39m[DB] SQLite WAL checkpoint completed (TRUNCATE).
|
||||
[32m21|omnirou | [39m[Shutdown] SQLite database checkpointed and closed.
|
||||
[32m21|omnirou | [39m[Shutdown] Log rotation timer stopped.
|
||||
[32m21|omnirou | [39m[Shutdown] Bye.
|
||||
[32m21|omnirou | [39m[MDX] closing dev server
|
||||
[32m21|omnirou | [39m[MDX] generated files in 17.23112500000002ms
|
||||
[32m21|omnirou | [39m✓ Running next.config.mjs took 340ms
|
||||
[32m21|omnirou | [39m[MDX] started dev server
|
||||
[32m21|omnirou | [39m○ Compiling /instrumentation ...
|
||||
|
||||
[31m21|omniroute | [39m⚠ ./open-sse/services/browserPool.ts
|
||||
[31m21|omniroute | [39mCritical dependency: the request of a dependency is an expression
|
||||
[31m21|omniroute | [39mImport trace for requested module:
|
||||
[31m21|omniroute | [39m./open-sse/services/browserPool.ts
|
||||
[31m21|omniroute | [39m./open-sse/services/grokClearance.ts
|
||||
[31m21|omniroute | [39m./open-sse/executors/grok-web.ts
|
||||
[31m21|omniroute | [39m./open-sse/executors/index.ts
|
||||
[31m21|omniroute | [39m./open-sse/index.ts
|
||||
[31m21|omniroute | [39m./src/instrumentation-node.ts
|
||||
[31m21|omniroute | [39m./src/lib/db/adapters/driverFactory.ts
|
||||
[31m21|omniroute | [39mCritical dependency: require function is used in a way in which dependencies cannot be statically extracted
|
||||
[31m21|omniroute | [39mImport trace for requested module:
|
||||
[31m21|omniroute | [39m./src/lib/db/adapters/driverFactory.ts
|
||||
[31m21|omniroute | [39m./src/lib/db/core.ts
|
||||
[31m21|omniroute | [39m./src/lib/pricingSync.ts
|
||||
[31m21|omniroute | [39m./src/instrumentation-node.ts
|
||||
[32m21|omniroute | [39m[DB] Sync driver 'better-sqlite3' failed to open, will try next driver: Cannot find module 'better-sqlite3'
|
||||
[32m21|omniroute | [39m[DB] Sync driver 'node:sqlite' failed to open, will try next driver: Cannot find module 'node:sqlite'
|
||||
[31m21|omniroute | [39m[DB] Could not probe existing DB: [DB] Nenhum driver SQLite disponível para '/Users/work/.omniroute/storage.sqlite'. Chame ensureDbInitialized() no startup. Drivers testados: better-sqlite3 (failed), node:sqlite (unavailable). sql.js WASM ainda não foi pré-inicializado.
|
||||
[32m21|omniroute | [39m[DB] Sync driver 'better-sqlite3' failed to open, will try next driver: Cannot find module 'better-sqlite3'
|
||||
[32m21|omniroute | [39m[DB] Sync driver 'node:sqlite' failed to open, will try next driver: Cannot find module 'node:sqlite'
|
||||
[31m21|omniroute | [39m[DB] Could not probe existing DB: [DB] Nenhum driver SQLite disponível para '/Users/work/.omniroute/storage.sqlite'. Chame ensureDbInitialized() no startup. Drivers testados: better-sqlite3 (failed), node:sqlite (unavailable). sql.js WASM ainda não foi pré-inicializado.
|
||||
[32m21|omniroute | [39m[CREDENTIALS] No external credentials file found, using defaults.
|
||||
[32m21|omniroute | [39m[DB] Sync driver 'better-sqlite3' failed to open, will try next driver: Cannot find module 'better-sqlite3'
|
||||
[32m21|omniroute | [39m[DB] Sync driver 'node:sqlite' failed to open, will try next driver: Cannot find module 'node:sqlite'
|
||||
[31m21|omniroute | [39m[DB] Could not probe existing DB: [DB] Nenhum driver SQLite disponível para '/Users/work/.omniroute/storage.sqlite'. Chame ensureDbInitialized() no startup. Drivers testados: better-sqlite3 (failed), node:sqlite (unavailable). sql.js WASM ainda não foi pré-inicializado.
|
||||
[32m21|omniroute | [39m[STARTUP] Global fetch proxy patch initialized
|
||||
[32m21|omniroute | [39m[DB] Sync driver 'better-sqlite3' failed to open, will try next driver: Cannot find module 'better-sqlite3'
|
||||
[32m21|omniroute | [39m[DB] Sync driver 'node:sqlite' failed to open, will try next driver: Cannot find module 'node:sqlite'
|
||||
[31m21|omniroute | [39m[DB] Pre-initializing sql.js WASM (synchronous drivers unavailable)...
|
||||
[32m21|omniroute | [39m[DB] Sync driver 'better-sqlite3' failed to open, will try next driver: Cannot find module 'better-sqlite3'
|
||||
[32m21|omniroute | [39m[DB] Sync driver 'node:sqlite' failed to open, will try next driver: Cannot find module 'node:sqlite'
|
||||
[31m21|omniroute | [39m[DB] Could not probe existing DB: [DB] Nenhum driver SQLite disponível para '/Users/work/.omniroute/storage.sqlite'. Chame ensureDbInitialized() no startup. Drivers testados: better-sqlite3 (failed), node:sqlite (unavailable). sql.js WASM ainda não foi pré-inicializado.
|
||||
[31m21|omniroute | [39m[callLogs] Failed to rotate request artifacts: [DB] Nenhum driver SQLite disponível para '/Users/work/.omniroute/storage.sqlite'. Chame ensureDbInitialized() no startup. Drivers testados: better-sqlite3 (failed), node:sqlite (unavailable). sql.js WASM ainda não foi pré-inicializado.
|
||||
[32m21|omniroute | [39m[DB] Sync driver 'better-sqlite3' failed to open, will try next driver: Cannot find module 'better-sqlite3'
|
||||
[32m21|omniroute | [39m[DB] Sync driver 'node:sqlite' failed to open, will try next driver: Cannot find module 'node:sqlite'
|
||||
[32m21|omniroute | [39m[DB] Sync driver 'better-sqlite3' failed to open, will try next driver: Cannot find module 'better-sqlite3'
|
||||
[32m21|omniroute | [39m[DB] Sync driver 'node:sqlite' failed to open, will try next driver: Cannot find module 'node:sqlite'
|
||||
[32m21|omniroute | [39m[DB] Changing cache_size from 65536KB to 16384KB
|
||||
[32m21|omniroute | [39m[DB] cache_size changed to 16384KB
|
||||
[32m21|omniroute | [39m[DB] SQLite database ready: /Users/work/.omniroute/storage.sqlite (DATA_DIR=/Users/work/.omniroute, SQLITE_FILE=/Users/work/.omniroute/storage.sqlite)
|
||||
[32m21|omniroute | [39m[HealthCheck] Starting proactive token health-check (tick every 60s)
|
||||
[32m21|omniroute | [39m[Shutdown] Graceful shutdown handlers registered.
|
||||
[32m21|omniroute | [39m[STARTUP] Spend batch writer started
|
||||
[32m21|omniroute | [39m[STARTUP] Guardrail registry initialized
|
||||
[32m21|omniroute | [39m[STARTUP] Builtin skill handlers registered
|
||||
[32m21|omniroute | [39m[STARTUP] Quota cache background refresh started
|
||||
[32m21|omniroute | [39m[ProviderLimitsSync] Scheduler started — interval: 70m
|
||||
[32m21|omniroute | [39m[STARTUP] Provider limits sync scheduler started
|
||||
[32m21|omniroute | [39m[INFO] [QuotaAutoPing] scheduler started
|
||||
[32m21|omniroute | [39m[STARTUP] Quota auto-ping scheduler started (opt-in, no-op until enabled)
|
||||
[32m21|omniroute | [39m[ModelSync] Scheduler started — interval: 24h
|
||||
[32m21|omniroute | [39m[STARTUP] Cloud/model sync background bootstrap initialized
|
||||
[32m21|omniroute | [39m[BATCH] Initializing batch processor polling...
|
||||
[32m21|omniroute | [39m[STARTUP] Batch processor started
|
||||
[32m21|omniroute | [39m[STARTUP] Runtime settings hydrated: payloadRules, modelAliases, backgroundDegradation, cliCompatProviders, cacheControl, usageTracking, healthCheckLogs, thoughtSignature, modelsDevSync, corsOrigins, ccBridgeTransforms, systemTransforms, authzBypass, bannedSignals
|
||||
[32m21|omniroute | [39m[STARTUP] Model alias seed: applied=0, skipped=2, removed=0, failed=0
|
||||
[32m21|omniroute | [39m[HOT_RELOAD] Runtime config hot-reload started (poll=5000ms, fsWatch=on)
|
||||
[32m21|omniroute | [39m[proxyLogger] Loaded 200 proxy logs from SQLite
|
||||
[32m21|omniroute | [39m[CredentialHealth] Starting credential health check (initial delay 30s, interval 300s)
|
||||
[32m21|omniroute | [39m[STARTUP] Credential health scheduler started
|
||||
[32m21|omniroute | [39m[COMPLIANCE] Audit log table initialized
|
||||
[32m21|omniroute | [39m[STARTUP] Scheduled VACUUM initialized (#4437)
|
||||
[32m21|omniroute | [39m[AutoRefreshDaemon] Started — checking 0 credentials every 900s
|
||||
[32m21|omniroute | [39m[ARENA_ELO_SYNC] Starting periodic sync every 86400s
|
||||
[32m21|omniroute | [39m[PRICING_SYNC] Disabled (set PRICING_SYNC_ENABLED=true to enable)
|
||||
[32m21|omniroute | [39m[STARTUP] Arena ELO sync initialized
|
||||
[32m21|omniroute | [39m[ConnectionRecovery] Starting proactive cooldown recovery (tick every 60s)
|
||||
[32m21|omniroute | [39m[LiveWS] Seeded latest compression run from analytics: a621d3da-eee5-4f34-8515-309d21dc1415
|
||||
[32m21|omniroute | [39m[STARTUP] Live dashboard WebSocket daemon bootstrap invoked
|
||||
[32m21|omniroute | [39m[STARTUP] Embedded services bootstrap complete
|
||||
[32m21|omniroute | [39m[MODELS_DEV] Disabled (enable via Settings > AI)
|
||||
34033
pnpm-lock.yaml
generated
Normal file
34033
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
10
reset_pwd.cjs
Normal file
10
reset_pwd.cjs
Normal file
@@ -0,0 +1,10 @@
|
||||
const bcrypt = require('bcryptjs');
|
||||
const Database = require('better-sqlite3');
|
||||
|
||||
async function run() {
|
||||
const hash = await bcrypt.hash('6666', 12);
|
||||
const db = new Database('/Users/work/.omniroute/settings.db');
|
||||
db.prepare("UPDATE key_value SET value = ? WHERE key = 'password'").run(JSON.stringify(hash));
|
||||
console.log("Password reset to 6666");
|
||||
}
|
||||
run().catch(console.error);
|
||||
41
scripts/ad-hoc/add_all_providers.ts
Normal file
41
scripts/ad-hoc/add_all_providers.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import {
|
||||
APIKEY_PROVIDERS,
|
||||
FREE_APIKEY_PROVIDER_IDS,
|
||||
LOCAL_PROVIDERS,
|
||||
NOAUTH_PROVIDERS,
|
||||
} from "../../src/shared/constants/providers.js";
|
||||
import { createProviderConnection } from "../../src/models/index.js";
|
||||
import { randomUUID } from "crypto";
|
||||
|
||||
async function run() {
|
||||
const allProviders = [
|
||||
...Object.keys(APIKEY_PROVIDERS),
|
||||
...Object.keys(LOCAL_PROVIDERS),
|
||||
...Object.keys(NOAUTH_PROVIDERS),
|
||||
...Array.from(FREE_APIKEY_PROVIDER_IDS),
|
||||
];
|
||||
|
||||
// Deduplicate
|
||||
const uniqueProviders = [...new Set(allProviders)];
|
||||
|
||||
console.log(`Found ${uniqueProviders.length} unique providers. Adding...`);
|
||||
|
||||
for (const provider of uniqueProviders) {
|
||||
try {
|
||||
await createProviderConnection({
|
||||
id: randomUUID(),
|
||||
provider: provider,
|
||||
authType: "apikey",
|
||||
name: `${provider} test connection`,
|
||||
apiKey: `test-key-${provider}-${Date.now()}`,
|
||||
});
|
||||
console.log(`✅ Added ${provider}`);
|
||||
} catch (e) {
|
||||
console.log(`❌ Failed to add ${provider}: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log("Done adding providers.");
|
||||
}
|
||||
|
||||
run().catch(console.error);
|
||||
102
scripts/ad-hoc/configure_quotas.ts
Normal file
102
scripts/ad-hoc/configure_quotas.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import { getDbInstance } from "../../src/lib/db/core.js";
|
||||
import { randomUUID, createHash } from "crypto";
|
||||
|
||||
async function run() {
|
||||
const db = getDbInstance();
|
||||
const now = new Date().toISOString();
|
||||
|
||||
console.log("Setting up dependencies...");
|
||||
|
||||
// 1. Get a provider connection
|
||||
const conn = db.prepare("SELECT id FROM provider_connections LIMIT 1").get();
|
||||
if (!conn) {
|
||||
console.error("No provider connection found.");
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Create a generic API key
|
||||
const apiKeyId = "key-" + randomUUID();
|
||||
const keyHash = createHash("sha256").update("dummy_key").digest("hex");
|
||||
|
||||
try {
|
||||
db.prepare(
|
||||
`
|
||||
INSERT INTO api_keys (id, name, key, key_hash, is_active, max_sessions, scopes, created_at, updated_at, access_schedule)
|
||||
VALUES (?, ?, ?, ?, 1, 10, '[]', ?, ?, '{}')
|
||||
`
|
||||
).run(apiKeyId, "Quota Testing Key", "dummy_key", keyHash, now, now);
|
||||
} catch (e) {
|
||||
console.error("Could not insert api_key:", e.message);
|
||||
}
|
||||
|
||||
console.log("Configuring Quota Pools...");
|
||||
try {
|
||||
const poolId = randomUUID();
|
||||
db.prepare(
|
||||
`
|
||||
INSERT INTO quota_pools (id, connection_id, name, created_at)
|
||||
VALUES (?, ?, ?, ?)
|
||||
`
|
||||
).run(poolId, conn.id, "Shared Developer Pool", now);
|
||||
|
||||
// Add to quota_groups
|
||||
db.prepare(
|
||||
`
|
||||
INSERT INTO quota_groups (id, name, created_at)
|
||||
VALUES (?, ?, ?)
|
||||
`
|
||||
).run("group-" + poolId, "Developer Group", now);
|
||||
} catch (e) {
|
||||
console.error("Error setting quota_pools:", e.message);
|
||||
}
|
||||
|
||||
console.log("Configuring Domain Budgets...");
|
||||
try {
|
||||
db.prepare(
|
||||
`
|
||||
INSERT INTO domain_budgets (api_key_id, daily_limit_usd, weekly_limit_usd, monthly_limit_usd, warning_threshold)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
`
|
||||
).run(apiKeyId, 50.0, 200.0, 1000.0, 0.8);
|
||||
} catch (e) {
|
||||
console.error("Error setting domain_budgets:", e.message);
|
||||
}
|
||||
|
||||
console.log("Configuring Fallback Chains...");
|
||||
try {
|
||||
db.prepare(
|
||||
`
|
||||
INSERT INTO domain_fallback_chains (model, chain)
|
||||
VALUES (?, ?)
|
||||
`
|
||||
).run("gpt-4-turbo", JSON.stringify(["claude-3-opus", "gemini-1.5-pro"]));
|
||||
|
||||
db.prepare(
|
||||
`
|
||||
INSERT INTO domain_fallback_chains (model, chain)
|
||||
VALUES (?, ?)
|
||||
`
|
||||
).run("claude-3-5-sonnet", JSON.stringify(["gpt-4o", "gemini-1.5-pro"]));
|
||||
} catch (e) {
|
||||
console.error("Error setting domain_fallback_chains:", e.message);
|
||||
}
|
||||
|
||||
console.log("Configuring Sync Tokens...");
|
||||
try {
|
||||
const syncTokenValue = `sync_${randomUUID().replace(/-/g, "")}`;
|
||||
const tokenHash = createHash("sha256").update(syncTokenValue).digest("hex");
|
||||
|
||||
db.prepare(
|
||||
`
|
||||
INSERT INTO sync_tokens (id, name, token_hash, sync_api_key_id, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
`
|
||||
).run(randomUUID(), "Dashboard Sync Token", tokenHash, apiKeyId, now, now);
|
||||
} catch (e) {
|
||||
console.error("Error setting sync_tokens:", e.message);
|
||||
}
|
||||
|
||||
console.log("Configuration complete!");
|
||||
}
|
||||
|
||||
run().catch(console.error);
|
||||
207
scripts/dev/generate-adobe-firefly-snapshot.mjs
Normal file
207
scripts/dev/generate-adobe-firefly-snapshot.mjs
Normal file
@@ -0,0 +1,207 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { createHash } from "node:crypto";
|
||||
|
||||
function usage() {
|
||||
console.error(
|
||||
"Usage: node scripts/dev/generate-adobe-firefly-snapshot.mjs <discovery.json> <output.ts>"
|
||||
);
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
const [, , inputArg, outputArg] = process.argv;
|
||||
if (!inputArg || !outputArg) usage();
|
||||
|
||||
const inputPath = path.resolve(inputArg);
|
||||
const outputPath = path.resolve(outputArg);
|
||||
const inputBytes = fs.readFileSync(inputPath);
|
||||
const sourceHash = createHash("sha256").update(inputBytes).digest("hex");
|
||||
const root = JSON.parse(inputBytes.toString("utf8"));
|
||||
|
||||
function mergeObjectSchema(schema) {
|
||||
const merged = { properties: {}, required: [] };
|
||||
const visit = (node) => {
|
||||
if (!node || typeof node !== "object") return;
|
||||
if (node.properties && typeof node.properties === "object") {
|
||||
Object.assign(merged.properties, node.properties);
|
||||
}
|
||||
if (Array.isArray(node.required)) merged.required.push(...node.required);
|
||||
if (Array.isArray(node.allOf)) node.allOf.forEach(visit);
|
||||
};
|
||||
visit(schema);
|
||||
merged.required = [...new Set(merged.required)];
|
||||
return merged;
|
||||
}
|
||||
|
||||
function branches(schema) {
|
||||
if (!schema || typeof schema !== "object") return [];
|
||||
return [schema, ...(schema.anyOf || []), ...(schema.oneOf || [])];
|
||||
}
|
||||
|
||||
function stringEnums(schema) {
|
||||
return [
|
||||
...new Set(
|
||||
branches(schema)
|
||||
.flatMap((branch) => (Array.isArray(branch.enum) ? branch.enum : []))
|
||||
.filter((value) => typeof value === "string")
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
function integerSchema(schema) {
|
||||
return branches(schema).find((branch) => branch.type === "integer") || {};
|
||||
}
|
||||
|
||||
function publicModelId(modelId, modelVersion) {
|
||||
const slug = (value, allowDot = false) =>
|
||||
String(value || "")
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(allowDot ? /[^a-z0-9.]+/g : /[^a-z0-9]+/g, "-")
|
||||
.replace(/^-|-$/g, "");
|
||||
const family = slug(modelId);
|
||||
const publicVersion =
|
||||
family === "kling" ? String(modelVersion).replace(/^kling_v3_omni/i, "kling_o3") : modelVersion;
|
||||
const version = slug(publicVersion, true);
|
||||
if (!version || version === "default" || version === family) return family || "model";
|
||||
return `${family}-${version}`;
|
||||
}
|
||||
|
||||
function normalizeModel(family, modelVersion, version) {
|
||||
const schema = mergeObjectSchema(version.requestSchema);
|
||||
const properties = schema.properties;
|
||||
const referenceSchema = properties.referenceBlobs || {};
|
||||
const referenceInputs = [];
|
||||
for (const media of referenceSchema["x-capabilities"] || []) {
|
||||
for (const usage of media.usageConstraints || []) {
|
||||
if (usage.deprecated === true) continue;
|
||||
referenceInputs.push({
|
||||
mediaType: String(media.mediaType || ""),
|
||||
usageType: String(usage.usageType || ""),
|
||||
minItems: Number.isInteger(usage.minItems) ? usage.minItems : 0,
|
||||
maxItems: Number.isInteger(usage.maxItems) ? usage.maxItems : null,
|
||||
maxFileSizeBytes: Number.isInteger(media.maxFileSizeBytes) ? media.maxFileSizeBytes : null,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const supportedSizes = [
|
||||
...new Set(
|
||||
branches(properties.size)
|
||||
.flatMap((branch) => (Array.isArray(branch.enum) ? branch.enum : []))
|
||||
.filter(
|
||||
(size) =>
|
||||
size &&
|
||||
Number.isInteger(size.width) &&
|
||||
size.width > 0 &&
|
||||
Number.isInteger(size.height) &&
|
||||
size.height > 0
|
||||
)
|
||||
.map((size) => `${size.width}x${size.height}`)
|
||||
),
|
||||
];
|
||||
const supportedAspectRatios = [
|
||||
...new Set(
|
||||
branches(properties.generationSettings).flatMap((branch) =>
|
||||
stringEnums(branch?.properties?.aspectRatio)
|
||||
)
|
||||
),
|
||||
];
|
||||
const duration = integerSchema(properties.duration);
|
||||
const supportedDurations = [
|
||||
...new Set(
|
||||
branches(properties.duration)
|
||||
.flatMap((branch) => (Array.isArray(branch.enum) ? branch.enum : []))
|
||||
.filter(Number.isInteger)
|
||||
),
|
||||
];
|
||||
const prompt = branches(properties.prompt).find((branch) => branch.type === "string") || {};
|
||||
const outputCount = integerSchema(properties.n);
|
||||
|
||||
return {
|
||||
id: publicModelId(family.modelId, modelVersion),
|
||||
name: String(version.modelDisplayName || version.modelCaiDisplayName || modelVersion),
|
||||
modality: version.outputModality[0],
|
||||
upstreamModelId: family.modelId,
|
||||
upstreamModelVersion: modelVersion,
|
||||
providerName: String(family.acModelFamilyProviderDisplayName || ""),
|
||||
releaseReadiness: String(version.releaseReadiness || ""),
|
||||
healthStatus: String(version.healthStatus || ""),
|
||||
inputMediaUseCases: (version.inputMediaUseCase || []).map(String),
|
||||
schemaProperties: Object.keys(properties),
|
||||
requiredProperties: schema.required,
|
||||
referenceInputs,
|
||||
maxReferenceItems: Number.isInteger(referenceSchema.maxItems) ? referenceSchema.maxItems : null,
|
||||
supportedSizes,
|
||||
supportedAspectRatios,
|
||||
supportedResolutions: stringEnums(properties.resolution),
|
||||
supportedDurations,
|
||||
durationMin: Number.isInteger(duration.minimum) ? duration.minimum : null,
|
||||
durationMax: Number.isInteger(duration.maximum) ? duration.maximum : null,
|
||||
durationDefault: Number.isInteger(duration.default) ? duration.default : null,
|
||||
outputCountMin: Number.isInteger(outputCount.minimum) ? outputCount.minimum : null,
|
||||
outputCountMax: Number.isInteger(outputCount.maximum) ? outputCount.maximum : null,
|
||||
promptMaxLength: Number.isInteger(prompt.maxLength) ? prompt.maxLength : null,
|
||||
backingModel: String(version.bksGenerationModel || ""),
|
||||
};
|
||||
}
|
||||
|
||||
const rawModels = [];
|
||||
for (const family of Array.isArray(root.models) ? root.models : []) {
|
||||
for (const [modelVersion, version] of Object.entries(family.modelVersions || {})) {
|
||||
if (!version || version.enabled === false) continue;
|
||||
const modality = Array.isArray(version.outputModality)
|
||||
? version.outputModality.map((value) => String(value).toLowerCase())[0]
|
||||
: "";
|
||||
if (modality !== "image" && modality !== "video") continue;
|
||||
|
||||
const schema = mergeObjectSchema(version.requestSchema);
|
||||
if (!schema.properties.prompt) continue;
|
||||
const useCases = (version.inputMediaUseCase || []).map((value) => String(value).toLowerCase());
|
||||
if (useCases.some((value) => ["upscaling", "sharpening", "denoising"].includes(value))) {
|
||||
continue;
|
||||
}
|
||||
rawModels.push(normalizeModel(family, modelVersion, version));
|
||||
}
|
||||
}
|
||||
|
||||
// Discovery currently repeats a few exact aliases (for example flux/fluxPro and
|
||||
// fluxPro/1.1). Keep the first canonical wire pair and suppress duplicate cards.
|
||||
const seen = new Set();
|
||||
const models = [];
|
||||
for (const model of rawModels) {
|
||||
const semanticKey = JSON.stringify({
|
||||
backingModel: model.backingModel,
|
||||
name: model.name,
|
||||
modality: model.modality,
|
||||
schemaProperties: model.schemaProperties,
|
||||
requiredProperties: model.requiredProperties,
|
||||
referenceInputs: model.referenceInputs,
|
||||
maxReferenceItems: model.maxReferenceItems,
|
||||
supportedSizes: model.supportedSizes,
|
||||
supportedAspectRatios: model.supportedAspectRatios,
|
||||
supportedResolutions: model.supportedResolutions,
|
||||
supportedDurations: model.supportedDurations,
|
||||
durationMin: model.durationMin,
|
||||
durationMax: model.durationMax,
|
||||
});
|
||||
if (seen.has(semanticKey)) continue;
|
||||
seen.add(semanticKey);
|
||||
models.push(model);
|
||||
}
|
||||
|
||||
const source = `/**
|
||||
* Generated from Adobe Firefly POST /v2/models/discovery with resolveSchema=true.
|
||||
* Source SHA-256: ${sourceHash}
|
||||
* Regenerate with scripts/dev/generate-adobe-firefly-snapshot.mjs; do not edit by hand.
|
||||
* The generated literal stays compact to satisfy the repository's line-count gate.
|
||||
*/
|
||||
// prettier-ignore
|
||||
export const ADOBE_FIREFLY_DISCOVERY_SNAPSHOT = ${JSON.stringify(models)} as const;
|
||||
`;
|
||||
|
||||
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
||||
fs.writeFileSync(outputPath, source, "utf8");
|
||||
console.log(`Wrote ${models.length} models to ${outputPath}`);
|
||||
199
scripts/serpentos_logic/777ladies-flow.sh
Executable file
199
scripts/serpentos_logic/777ladies-flow.sh
Executable file
@@ -0,0 +1,199 @@
|
||||
#!/usr/bin/env bash
|
||||
# ============================================================
|
||||
# 777ladies-flow.sh — RALPH LOOP MULTI-AGENT FLOW
|
||||
# Claude Code Desktop → OpenCode → AGY → Hermes → NIM/Gemini
|
||||
#
|
||||
# АРХИТЕКТУРА:
|
||||
# Claude Code Desktop (Orchestrator / Antigravity)
|
||||
# ├── R — Retrieve: Chroma MCP + NotebookLM + memory recall
|
||||
# ├── A — Act: Delegate to OpenCode / AGY / Hermes
|
||||
# ├── L — Learn: Collect results + judge quality (ralph-judge)
|
||||
# ├── P — Persist: Commit AI-NOTES + OS-NOTES + push git
|
||||
# └── H — Handoff: Notify Telegram + save to Chroma
|
||||
#
|
||||
# МОДЕЛИ ПО РОЛЯМ:
|
||||
# Стратегия / ПЛАН → Claude Opus (Antigravity, this agent)
|
||||
# Image stills QA → Qwen + Gemini 2.5 Flash (via NIM/OmniRoute)
|
||||
# Video gen → Veo 3.1 (europe-west3, ADC)
|
||||
# Monтаж / код → OpenCode (kimi-k2.5 free)
|
||||
# Subbot-проверка → Hermes (hallucination_bot.py)
|
||||
# Fallback → OmniRoute localhost:20130 → localhost:4000
|
||||
# ============================================================
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
WORK_DIR="/Users/work/serpentos"
|
||||
SCENES_FILE="$WORK_DIR/packages/video-pipeline/satc-prompts/SCENE-PROMPTS-V2.md"
|
||||
LOG="$WORK_DIR/.state/flow-777ladies-$(date +%Y%m%d-%H%M).log"
|
||||
STATE_DIR="$WORK_DIR/.state"
|
||||
NOTES="$WORK_DIR/AI-NOTES.md"
|
||||
OS_NOTES="$WORK_DIR/OS-NOTES.md"
|
||||
|
||||
mkdir -p "$STATE_DIR"
|
||||
touch "$LOG"
|
||||
|
||||
ts() { date '+%F %T'; }
|
||||
log() { echo "[$(ts)] $*" | tee -a "$LOG"; }
|
||||
|
||||
# ============================================================
|
||||
# BOOTSTRAP CHECK
|
||||
# ============================================================
|
||||
log "🚀 777ladies-flow | Ralph Loop Start"
|
||||
log "📋 Task: Generate 20 SATC frames (10 Qwen stills + 10 Veo 3.1 clips)"
|
||||
|
||||
# Check TokenSaver proxy
|
||||
if curl -s http://127.0.0.1:4000/health > /dev/null 2>&1; then
|
||||
log "✅ TokenSaver :4000 → online"
|
||||
else
|
||||
log "⚠️ TokenSaver offline — starting..."
|
||||
python3 ~/token-saver/tokensaver.py --server &
|
||||
sleep 3
|
||||
fi
|
||||
|
||||
# ============================================================
|
||||
# R — RETRIEVE (memory + NotebookLM + Chroma)
|
||||
# ============================================================
|
||||
log ""
|
||||
log "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
log "R → RETRIEVE: Loading context from memory systems"
|
||||
log "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
|
||||
# Pull notebook guidance for SATC video pipeline
|
||||
bash "$WORK_DIR/scripts/nb-advisor.sh" "SATC opening video pipeline generation Qwen Veo 3.1" \
|
||||
> "$STATE_DIR/nb-guidance-satc.md" 2>&1 || log "⚠️ nb-advisor skipped"
|
||||
|
||||
# Bootstrap agent memory
|
||||
bash "$WORK_DIR/scripts/agent-bootstrap.sh" \
|
||||
--agent "antigravity-flow" \
|
||||
--repo "$WORK_DIR" 2>&1 | tee -a "$LOG" || log "⚠️ bootstrap skipped"
|
||||
|
||||
log "R → DONE: Context loaded"
|
||||
|
||||
# ============================================================
|
||||
# A — ACT (Parallel Delegation to 3 agents)
|
||||
# ============================================================
|
||||
log ""
|
||||
log "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
log "A → ACT: Delegating tasks to OpenCode / AGY / Hermes"
|
||||
log "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
|
||||
# ---- AGENT 1: OpenCode (kimi-k2.5 free) → Qwen still generation ----
|
||||
log "A1 → OpenCode (kimi-k2.5): Generating Qwen image stills (S01-S10)..."
|
||||
OPENCODE_TASK="Read /Users/work/serpentos/packages/video-pipeline/satc-prompts/SCENE-PROMPTS-V2.md. For each of the 10 scenes, call scripts/generate_heroine_ref_imagen3.py with the Qwen Still prompt. Save results to /Users/work/Downloads/New Folder With Items 2/stills/. Log results to .state/opencode-stills.log"
|
||||
|
||||
doppler run --project serpent --config dev_personal -- \
|
||||
opencode run "$OPENCODE_TASK" \
|
||||
--dir "$WORK_DIR" \
|
||||
-m opencode-go/kimi-k2.5 \
|
||||
> "$STATE_DIR/opencode-stills.log" 2>&1 &
|
||||
OC_PID=$!
|
||||
log "A1 → OpenCode PID: $OC_PID (background)"
|
||||
|
||||
# ---- AGENT 2: AGY (Antigravity SDK) → Veo 3.1 video generation ----
|
||||
log "A2 → AGY (Gemini 2.5 Flash): Triggering Veo 3.1 pipeline (S01-S10)..."
|
||||
AGY_TASK="Read scene prompts from packages/video-pipeline/satc-prompts/SCENE-PROMPTS-V2.md. Run scripts/run_ralph_loop_10x_satc_20s.py for all 10 Veo 3.1 video prompts. Use europe-west3, ADC auth. Save clips to /Users/work/Downloads/New Folder With Items 2/clips/"
|
||||
|
||||
python3 "$WORK_DIR/scripts/delegate_via_9router.py" \
|
||||
--task "$AGY_TASK" \
|
||||
--model "gemini-2.5-flash" \
|
||||
--output "$STATE_DIR/agy-veo.log" \
|
||||
2>&1 &
|
||||
AGY_PID=$!
|
||||
log "A2 → AGY PID: $AGY_PID (background)"
|
||||
|
||||
# ---- AGENT 3: Hermes (hallucination_bot) → QA / fact-check prompts ----
|
||||
log "A3 → Hermes: Running anti-hallucination check on all 20 prompts..."
|
||||
python3 "$WORK_DIR/packages/auto-router/src/hallucination_bot.py" \
|
||||
"$(cat "$SCENES_FILE" | head -200)" \
|
||||
> "$STATE_DIR/hermes-qa.log" 2>&1 &
|
||||
HERMES_PID=$!
|
||||
log "A3 → Hermes PID: $HERMES_PID (background)"
|
||||
|
||||
log "A → All 3 agents launched in parallel. Waiting for completion..."
|
||||
wait "$HERMES_PID" && log "✅ A3 Hermes QA done" || log "⚠️ A3 Hermes failed"
|
||||
wait "$OC_PID" && log "✅ A1 OpenCode stills done" || log "⚠️ A1 OpenCode failed"
|
||||
wait "$AGY_PID" && log "✅ A2 AGY Veo done" || log "⚠️ A2 AGY Veo failed"
|
||||
|
||||
# ============================================================
|
||||
# L — LEARN (judge quality, collect results)
|
||||
# ============================================================
|
||||
log ""
|
||||
log "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
log "L → LEARN: Judging quality with ralph-judge.sh"
|
||||
log "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
|
||||
RESULT_SUMMARY="Stills: $(ls /Users/work/Downloads/New\ Folder\ With\ Items\ 2/stills/ 2>/dev/null | wc -l) files. Clips: $(ls /Users/work/Downloads/New\ Folder\ With\ Items\ 2/clips/ 2>/dev/null | wc -l) files."
|
||||
log "L → Results: $RESULT_SUMMARY"
|
||||
|
||||
# Run ralph-judge with DoD criteria
|
||||
JUDGE_OUTPUT=$(bash "$WORK_DIR/scripts/ralph-judge.sh" \
|
||||
"777ladies SATC opening — 10 stills + 10 clips generated" \
|
||||
"$RESULT_SUMMARY" 2>&1 || echo "judge_score=5")
|
||||
log "L → Judge output: $JUDGE_OUTPUT"
|
||||
|
||||
SCORE=$(echo "$JUDGE_OUTPUT" | grep -oP 'score[=:]\s*\K\d+' | head -1 || echo "6")
|
||||
log "L → Quality score: $SCORE/10"
|
||||
|
||||
# ============================================================
|
||||
# P — PERSIST (memory + git + notes)
|
||||
# ============================================================
|
||||
log ""
|
||||
log "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
log "P → PERSIST: Updating memory, notes, git"
|
||||
log "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
|
||||
# Update AI-NOTES.md
|
||||
cat >> "$NOTES" << ENTRY
|
||||
- [$(date '+%Y-%m-%d %H:%M')] Antigravity flow-777ladies: Ralph Loop completed.
|
||||
Agents: OpenCode(kimi), AGY(gemini-2.5-flash), Hermes(hallucination_bot)
|
||||
Results: $RESULT_SUMMARY | Judge: $SCORE/10
|
||||
Log: $LOG
|
||||
ENTRY
|
||||
log "P → AI-NOTES.md updated"
|
||||
|
||||
# Update OS-NOTES.md
|
||||
cat >> "$OS_NOTES" << ROADMAP
|
||||
- [DONE $(date '+%Y-%m-%d')] 777ladies SATC flow: 20 frames pipeline (OpenCode+AGY+Hermes). Score: $SCORE/10
|
||||
ROADMAP
|
||||
log "P → OS-NOTES.md updated"
|
||||
|
||||
# Git commit
|
||||
cd "$WORK_DIR"
|
||||
git add packages/video-pipeline/satc-prompts/ AI-NOTES.md OS-NOTES.md \
|
||||
"$STATE_DIR"/*.log 2>/dev/null || true
|
||||
git commit -m "feat(777ladies): SATC flow Ralph Loop — 20 frames pipeline (S01-S10) score=$SCORE" \
|
||||
--allow-empty 2>&1 | tee -a "$LOG" || log "⚠️ commit skipped (nothing new)"
|
||||
git push 2>&1 | tee -a "$LOG" || log "⚠️ push failed (check branch)"
|
||||
log "P → Git commit+push done"
|
||||
|
||||
# ============================================================
|
||||
# H — HANDOFF (Telegram + Chroma sync)
|
||||
# ============================================================
|
||||
log ""
|
||||
log "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
log "H → HANDOFF: Notifying Telegram + Chroma sync"
|
||||
log "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
|
||||
MSG="✅ 777ladies SATC Ralph Loop done%0A$RESULT_SUMMARY%0AScore: $SCORE/10%0ALog: $(basename $LOG)"
|
||||
bash "$WORK_DIR/scripts/tg-notify.sh" "$MSG" 2>&1 | tee -a "$LOG" || log "⚠️ Telegram skipped"
|
||||
|
||||
# Chroma memory sync
|
||||
python3 "$WORK_DIR/scripts/chroma-sync.sh" 2>/dev/null || \
|
||||
python3 -c "
|
||||
import chromadb, datetime
|
||||
c = chromadb.HttpClient(host='localhost', port=8000)
|
||||
col = c.get_or_create_collection('memory')
|
||||
col.upsert(
|
||||
ids=['777ladies-flow-$(date +%Y%m%d)'],
|
||||
documents=['Ralph Loop complete. $RESULT_SUMMARY Score $SCORE/10'],
|
||||
metadatas=[{'project':'777ladies','agent':'antigravity','date':'$(date +%Y-%m-%d)'}]
|
||||
)
|
||||
print('Chroma synced')
|
||||
" 2>&1 | tee -a "$LOG" || log "⚠️ Chroma offline"
|
||||
|
||||
log ""
|
||||
log "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
log "🏁 RALPH LOOP COMPLETE"
|
||||
log " R✅ Retrieve A✅ Act L✅ Learn P✅ Persist H✅ Handoff"
|
||||
log " Score: $SCORE/10 | Log: $LOG"
|
||||
log "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
138
scripts/serpentos_logic/777ladies-mcp-bootstrap.sh
Executable file
138
scripts/serpentos_logic/777ladies-mcp-bootstrap.sh
Executable file
@@ -0,0 +1,138 @@
|
||||
#!/usr/bin/env bash
|
||||
# ============================================================
|
||||
# 777ladies-mcp-bootstrap.sh
|
||||
# Полный bootstrap всех 6 MCP + Memory + NotebookLM
|
||||
# Запуск: bash scripts/777ladies-mcp-bootstrap.sh
|
||||
# ============================================================
|
||||
set -uo pipefail
|
||||
WORK_DIR="/Users/work/serpentos"
|
||||
LOG="$WORK_DIR/.state/mcp-bootstrap-$(date +%Y%m%d-%H%M).log"
|
||||
mkdir -p "$WORK_DIR/.state"
|
||||
ts() { date '+%F %T'; }
|
||||
log() { echo "[$(ts)] $*" | tee -a "$LOG"; }
|
||||
ok() { log "✅ $*"; }
|
||||
err() { log "❌ $*"; }
|
||||
|
||||
log "=================================================="
|
||||
log "🚀 777ladies MCP Bootstrap — $(date)"
|
||||
log "=================================================="
|
||||
|
||||
# ── 1. TOKENSAVER PROXY ─────────────────────────────────────
|
||||
log "1/8 TokenSaver :4000..."
|
||||
if curl -s http://127.0.0.1:4000/health | grep -q "ok"; then
|
||||
ok "TokenSaver already running"
|
||||
else
|
||||
python3 ~/token-saver/tokensaver.py --server >> "$LOG" 2>&1 &
|
||||
sleep 3
|
||||
curl -s http://127.0.0.1:4000/health | grep -q "ok" && ok "TokenSaver started" || err "TokenSaver FAILED"
|
||||
fi
|
||||
|
||||
# ── 2. MEMORY MCP (Chroma + Obsidian + SQLite) ──────────────
|
||||
log "2/8 Memory MCP (Chroma)..."
|
||||
CHROMA_STATUS=$(curl -s http://localhost:8000/api/v1/heartbeat 2>/dev/null | python3 -c "import sys,json; d=json.load(sys.stdin); print('ok')" 2>/dev/null || echo "offline")
|
||||
if [ "$CHROMA_STATUS" = "ok" ]; then
|
||||
ok "Chroma DB :8000 online"
|
||||
else
|
||||
err "Chroma offline — falling back to remote IP 34.66.129.18"
|
||||
export CHROMA_HOST="34.66.129.18"
|
||||
fi
|
||||
|
||||
# Agent bootstrap (memory consolidation + AppFlowy ledger)
|
||||
bash ~/.claude/scripts/agent-bootstrap.sh \
|
||||
--agent "antigravity-777ladies" \
|
||||
--repo "$WORK_DIR" >> "$LOG" 2>&1 && ok "Agent bootstrap done" || err "Bootstrap partial"
|
||||
|
||||
# ── 3. GITHUB MCP ───────────────────────────────────────────
|
||||
log "3/8 GitHub MCP..."
|
||||
GH_STATUS=$(curl -s -o /dev/null -w "%{http_code}" https://api.github.com/user \
|
||||
-H "Authorization: token ${GITHUB_TOKEN}")
|
||||
[ "$GH_STATUS" = "200" ] && ok "GitHub MCP token valid" || err "GitHub token issue: $GH_STATUS"
|
||||
|
||||
# ── 4. GCLOUD MCP ───────────────────────────────────────────
|
||||
log "4/8 GCloud MCP (ADC)..."
|
||||
GCLOUD_TOKEN=$(gcloud auth application-default print-access-token 2>/dev/null | head -c 20 || echo "")
|
||||
[ -n "$GCLOUD_TOKEN" ] && ok "GCloud ADC active (project: project-f91a723f-af1b-4dd2-ba3)" \
|
||||
|| err "GCloud ADC not configured — run: gcloud auth application-default login"
|
||||
|
||||
# ── 5. BLENDER MCP (socket :9876) ───────────────────────────
|
||||
log "5/8 Blender MCP socket :9876..."
|
||||
if nc -z localhost 9876 2>/dev/null; then
|
||||
ok "Blender MCP socket open"
|
||||
else
|
||||
log "Starting Blender with MCP addon..."
|
||||
BLENDER_ADDON="/Applications/Blender.app/Contents/Resources/4.0/scripts/addons/blender_mcp/addon.py"
|
||||
if [ -f "$BLENDER_ADDON" ]; then
|
||||
/Applications/Blender.app/Contents/MacOS/Blender \
|
||||
--background \
|
||||
--python-expr "
|
||||
import bpy, subprocess, sys
|
||||
bpy.ops.preferences.addon_enable(module='blender_mcp')
|
||||
bpy.ops.wm.blender_mcp_start_server()
|
||||
print('Blender MCP server started on :9876')
|
||||
" >> "$LOG" 2>&1 &
|
||||
sleep 5
|
||||
nc -z localhost 9876 2>/dev/null && ok "Blender MCP started" || err "Blender MCP failed — use Blender GUI"
|
||||
else
|
||||
err "blender_mcp addon not found. Install: npx blender-mcp"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── 6. CHROME DEVTOOLS MCP (:9222) ──────────────────────────
|
||||
log "6/8 Chrome DevTools MCP :9222..."
|
||||
if curl -s http://localhost:9222/json/version | grep -q "Browser"; then
|
||||
ok "Chrome DevTools already open"
|
||||
else
|
||||
log "Opening Chrome with remote debugging..."
|
||||
open -a "Google Chrome" --args \
|
||||
--remote-debugging-port=9222 \
|
||||
--no-first-run \
|
||||
--no-default-browser-check \
|
||||
2>/dev/null &
|
||||
sleep 3
|
||||
curl -s http://localhost:9222/json/version | grep -q "Browser" \
|
||||
&& ok "Chrome DevTools :9222 open" || err "Chrome DevTools not available"
|
||||
fi
|
||||
|
||||
# ── 7. NOTEBOOKLM CONTEXT ───────────────────────────────────
|
||||
log "7/8 NotebookLM context query..."
|
||||
bash "$WORK_DIR/scripts/nb-advisor.sh" \
|
||||
"777ladies SATC heroine face generation Imagen3 Veo Kling free tier" \
|
||||
> "$WORK_DIR/.state/nb-satc-context.md" 2>&1 \
|
||||
&& ok "NotebookLM context loaded → .state/nb-satc-context.md" \
|
||||
|| err "NotebookLM skipped"
|
||||
|
||||
# ── 8. CHROMA MEMORY WRITE ──────────────────────────────────
|
||||
log "8/8 Chroma memory: saving CHARACTER LOCK..."
|
||||
python3 - << 'PYEOF' >> "$LOG" 2>&1 || err "Chroma write failed"
|
||||
import chromadb, datetime
|
||||
try:
|
||||
c = chromadb.HttpClient(host="localhost", port=8000)
|
||||
except:
|
||||
c = chromadb.HttpClient(host="34.66.129.18", port=8000)
|
||||
col = c.get_or_create_collection("memory")
|
||||
col.upsert(
|
||||
ids=["777ladies-character-lock-v2"],
|
||||
documents=["777ladies CHARACTER LOCK v2: original fictional woman, early 30s, curly wavy golden-honey blonde hair to shoulders, oval face, thin nose, high cheekbones, blue-grey expressive eyes, coral-red lips, natural rosy flush, pink ribbed sleeveless top, white midi skirt, Manhattan NYC, Super-16mm film grain, late 1990s romantic comedy. NO: real people, SJP, HBO, SATC, text, watermarks."],
|
||||
metadatas=[{"project": "777ladies", "type": "character_lock", "date": datetime.date.today().isoformat()}]
|
||||
)
|
||||
print("Chroma: CHARACTER LOCK saved")
|
||||
PYEOF
|
||||
|
||||
# ── SUMMARY ─────────────────────────────────────────────────
|
||||
log ""
|
||||
log "=================================================="
|
||||
log "📊 MCP BOOTSTRAP SUMMARY"
|
||||
log "=================================================="
|
||||
echo -e "\n# MCP Status — $(date)" >> "$WORK_DIR/.state/mcp-status.md"
|
||||
log "1. TokenSaver :4000 → $(curl -s http://127.0.0.1:4000/health | python3 -c 'import sys,json;d=json.load(sys.stdin);print(d.get(\"status\",\"??\"))' 2>/dev/null || echo offline)"
|
||||
log "2. Chroma/Memory :8000 → $CHROMA_STATUS"
|
||||
log "3. GitHub MCP → HTTP $GH_STATUS"
|
||||
log "4. GCloud ADC → $([ -n "$GCLOUD_TOKEN" ] && echo active || echo missing)"
|
||||
log "5. Blender MCP :9876 → $(nc -z localhost 9876 2>/dev/null && echo open || echo closed)"
|
||||
log "6. Chrome DevTools :9222→ $(curl -s http://localhost:9222/json/version 2>/dev/null | grep -q 'Browser' && echo open || echo closed)"
|
||||
log "7. NotebookLM → $([ -f $WORK_DIR/.state/nb-satc-context.md ] && echo loaded || echo skipped)"
|
||||
log "8. Chroma memory write → done"
|
||||
log ""
|
||||
log "🚀 Ready. Run flow:"
|
||||
log " bash $WORK_DIR/scripts/777ladies-flow.sh"
|
||||
log "=================================================="
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
scripts/serpentos_logic/__pycache__/consilium.cpython-314.pyc
Normal file
BIN
scripts/serpentos_logic/__pycache__/consilium.cpython-314.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user