mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-12 10:12:11 +03:00
Compare commits
9 Commits
fix/securi
...
feat/condu
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
552c969759 | ||
|
|
4d6c855258 | ||
|
|
7b951761fc | ||
|
|
b97318d73b | ||
|
|
a75295f359 | ||
|
|
00e15af622 | ||
|
|
5eb10e896d | ||
|
|
33baf62b58 | ||
|
|
d42a58141b |
@@ -1,11 +0,0 @@
|
||||
- **feat(plugins):** add onStreamComplete built-in event exposing streaming usage and timing (#9571)
|
||||
|
||||
Adds a new `onStreamComplete` plugin event that fires after an SSE stream is fully
|
||||
consumed, carrying usage token counts and timing metrics (latency, TTFT). Built-in
|
||||
events now include `onStreamComplete` as a fire-and-forget lifecycle hook.
|
||||
|
||||
Payload: `status`, `usage` (prompt_tokens, completion_tokens, reasoning_tokens,
|
||||
cache_read_input_tokens, cache_creation_input_tokens), `timing` (latencyMs, ttft),
|
||||
`model`, `provider`, `errorCode`.
|
||||
|
||||
Non-breaking — existing `onResponse` hooks with `{ streamed: true }` remain unchanged.
|
||||
@@ -1 +0,0 @@
|
||||
- feat(a2a): inbound delegation to the OmniConductor fleet — `POST /api/a2a/tasks` translates an external A2A task into the hub's `POST /v1/tasks` (fleet skills only, repo required, `CONDUCTOR_ORCHESTRATOR_TOKEN` with hub-token fallback); states flow back through the SSE→A2A mirror
|
||||
@@ -1 +0,0 @@
|
||||
- feat(dashboard): "Conductor" panel — OmniConductor fleet (runners + task queue) live via server-side proxy routes (`/api/conductor/*`, management auth, hub token never reaches the browser), task detail with manifest/council and cancel-with-confirmation; sidebar entry under Tools
|
||||
@@ -1 +0,0 @@
|
||||
- feat(dashboard): Faro chat with voice on the Conductor panel — text via `/api/conductor/ask` (server-side proxy to the spokesperson; hub credential never reaches the browser; `pending` → Sim/Não confirmation buttons) and a guaranteed push-to-talk voice cycle (MediaRecorder → `/api/v1/audio/transcriptions` → ask → `/api/v1/audio/speech` playback), with operator-configurable STT/TTS models
|
||||
@@ -178,9 +178,6 @@ The JSON-RPC endpoint `/a2a` is the canonical A2A entry point. The REST endpoint
|
||||
| `/api/a2a/tasks/[id]` | GET | Get task by ID | management |
|
||||
| `/api/a2a/tasks/[id]/cancel` | POST | Cancel running task | management |
|
||||
| `/.well-known/agent.json` | GET | Agent Card (A2A discovery) | (public, cached 3600s) |
|
||||
| `/api/a2a/tasks` | POST | Inbound delegation to the OmniConductor fleet (Conductor PRD RF5) | Bearer vs `OMNIROUTE_API_KEY` + `a2aEnabled` |
|
||||
|
||||
**Inbound Conductor delegation (`POST /api/a2a/tasks`):** external A2A agents delegate coding work to the OmniConductor fleet through OmniRoute. Body: `{ skill: "conductor" | "conductor-cli-<profile>", messages: [{role, content}], metadata: { conductor: { repo: { url, base_ref? }, mode?, cli?, model? } } }` — only Conductor fleet skills (the ones announced on the Agent Card) are delegable; `metadata.conductor.repo.url` is required (the fleet works on git repos). The route translates to the hub's `POST /v1/tasks` using the server-side `CONDUCTOR_ORCHESTRATOR_TOKEN` (fallback `CONDUCTOR_HUB_TOKEN`) and returns `201 { conductor_task_id, state: "submitted" }`; task states flow back through the SSE→A2A mirror (RF1) and are visible via `GET /api/a2a/tasks?skill=conductor`.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -233,32 +233,6 @@ import { hyperagentProvider } from "./registry/hyperagent/index.ts";
|
||||
import { muse_codeProvider } from "./registry/muse-code/index.ts";
|
||||
import { naga_acProvider } from "./registry/naga-ac/index.ts";
|
||||
import { chatanywhereProvider } from "./registry/chatanywhere/index.ts";
|
||||
import { zyloApiProvider } from "./registry/zylo-api/index.ts";
|
||||
import { poolsideProvider } from "./registry/poolside/index.ts";
|
||||
import { fastrouterProvider } from "./registry/fastrouter/index.ts";
|
||||
import { anyapiProvider } from "./registry/anyapi/index.ts";
|
||||
import { electronhubProvider } from "./registry/electronhub/index.ts";
|
||||
import { llmgatewayProvider } from "./registry/llmgateway/index.ts";
|
||||
import { llmKiwiProvider } from "./registry/llm-kiwi/index.ts";
|
||||
import { literouterProvider } from "./registry/literouter/index.ts";
|
||||
import { mnnAiProvider } from "./registry/mnn-ai/index.ts";
|
||||
import { meganovaAiProvider } from "./registry/meganova-ai/index.ts";
|
||||
import { mixlayerProvider } from "./registry/mixlayer/index.ts";
|
||||
import { spekaProvider } from "./registry/speka/index.ts";
|
||||
import { tokenreplyProvider } from "./registry/tokenreply/index.ts";
|
||||
import { yoloAutoProvider } from "./registry/yolo-auto/index.ts";
|
||||
import { dxntProvider } from "./registry/dxnt/index.ts";
|
||||
import { cloudcodeOneProvider } from "./registry/cloudcode-one/index.ts";
|
||||
import { ofoxaiProvider } from "./registry/ofoxai/index.ts";
|
||||
import { zerolimitaiProvider } from "./registry/zerolimitai/index.ts";
|
||||
import { helyxaiProvider } from "./registry/helyxai/index.ts";
|
||||
import { aurikoProvider } from "./registry/auriko/index.ts";
|
||||
import { poixeAiProvider } from "./registry/poixe-ai/index.ts";
|
||||
import { nagaAiProvider } from "./registry/naga-ai/index.ts";
|
||||
import { chatOripeProvider } from "./registry/chat-oripe/index.ts";
|
||||
import { freeinferenceProvider } from "./registry/freeinference/index.ts";
|
||||
import { freeAiProvider } from "./registry/free-ai/index.ts";
|
||||
|
||||
|
||||
export const REGISTRY: Record<string, RegistryEntry> = {
|
||||
aimlapi: aimlapiProvider,
|
||||
@@ -491,33 +465,7 @@ export const REGISTRY: Record<string, RegistryEntry> = {
|
||||
promptql: promptqlProvider,
|
||||
hyperagent: hyperagentProvider,
|
||||
"muse-code": muse_codeProvider,
|
||||
"zylo-api": zyloApiProvider,
|
||||
unorouter: unorouterProvider,
|
||||
"naga-ac": naga_acProvider,
|
||||
chatanywhere: chatanywhereProvider,
|
||||
poolside: poolsideProvider,
|
||||
fastrouter: fastrouterProvider,
|
||||
anyapi: anyapiProvider,
|
||||
electronhub: electronhubProvider,
|
||||
llmgateway: llmgatewayProvider,
|
||||
"llm-kiwi": llmKiwiProvider,
|
||||
literouter: literouterProvider,
|
||||
"mnn-ai": mnnAiProvider,
|
||||
"meganova-ai": meganovaAiProvider,
|
||||
mixlayer: mixlayerProvider,
|
||||
speka: spekaProvider,
|
||||
tokenreply: tokenreplyProvider,
|
||||
"yolo-auto": yoloAutoProvider,
|
||||
dxnt: dxntProvider,
|
||||
"cloudcode-one": cloudcodeOneProvider,
|
||||
ofoxai: ofoxaiProvider,
|
||||
zerolimitai: zerolimitaiProvider,
|
||||
helyxai: helyxaiProvider,
|
||||
auriko: aurikoProvider,
|
||||
"poixe-ai": poixeAiProvider,
|
||||
"naga-ai": nagaAiProvider,
|
||||
"chat-oripe": chatOripeProvider,
|
||||
freeinference: freeinferenceProvider,
|
||||
"free-ai": freeAiProvider,
|
||||
|
||||
};
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
import type { RegistryEntry } from "../../shared.ts";
|
||||
import { buildOpenAiCompatibleRegistryEntry } from "../../shared.ts";
|
||||
|
||||
export const aurikoProvider: RegistryEntry = buildOpenAiCompatibleRegistryEntry({
|
||||
id: "auriko",
|
||||
alias: "auriko",
|
||||
baseUrl: "https://api.auriko.ai/v1/chat/completions",
|
||||
modelsUrl: "https://api.auriko.ai/v1/models",
|
||||
models: [],
|
||||
passthroughModels: true,
|
||||
});
|
||||
@@ -1,12 +0,0 @@
|
||||
import type { RegistryEntry } from "../../shared.ts";
|
||||
import { buildOpenAiCompatibleRegistryEntry } from "../../shared.ts";
|
||||
|
||||
// The upstream brand and hostname are ambiguous, so avoid unverified quota claims.
|
||||
export const chatOripeProvider: RegistryEntry = buildOpenAiCompatibleRegistryEntry({
|
||||
id: "chat-oripe",
|
||||
alias: "chat-oripe",
|
||||
baseUrl: "https://api.oriper.com/v1/chat/completions",
|
||||
modelsUrl: "https://api.oriper.com/v1/models",
|
||||
models: [],
|
||||
passthroughModels: true,
|
||||
});
|
||||
@@ -1,12 +1,17 @@
|
||||
import type { RegistryEntry } from "../../shared.ts";
|
||||
import { buildOpenAiCompatibleRegistryEntry } from "../../shared.ts";
|
||||
|
||||
// International endpoint; audited free access is limited to non-commercial use.
|
||||
export const chatanywhereProvider: RegistryEntry = buildOpenAiCompatibleRegistryEntry({
|
||||
// ChatAnywhere (api.chatanywhere.tech) — OpenAI-compatible gateway from the
|
||||
// chatanywhere/GPT_API_free project (~38.7k GitHub stars). Requires GitHub-account-
|
||||
// gated API key. Free tier is for personal non-commercial use only.
|
||||
export const chatanywhereProvider: RegistryEntry = {
|
||||
id: "chatanywhere",
|
||||
alias: "chatanywhere",
|
||||
baseUrl: "https://api.chatanywhere.org/v1/chat/completions",
|
||||
modelsUrl: "https://api.chatanywhere.org/v1/models",
|
||||
models: [],
|
||||
alias: "chtany",
|
||||
format: "openai",
|
||||
executor: "default",
|
||||
baseUrl: "https://api.chatanywhere.tech/v1/chat/completions",
|
||||
modelsUrl: "https://api.chatanywhere.tech/v1/models",
|
||||
authType: "apikey",
|
||||
authHeader: "bearer",
|
||||
passthroughModels: true,
|
||||
});
|
||||
models: [],
|
||||
};
|
||||
@@ -1,20 +0,0 @@
|
||||
import type { RegistryEntry } from "../../shared.ts";
|
||||
import { buildOpenAiCompatibleRegistryEntry } from "../../shared.ts";
|
||||
|
||||
/**
|
||||
* CloudCode.ONE - OpenAI-compatible API with published free model aliases.
|
||||
*
|
||||
* The Anthropic-compatible endpoint is documented separately; this registry
|
||||
* covers the OpenAI-compatible API surface audited for this migration.
|
||||
*/
|
||||
export const cloudcodeOneProvider: RegistryEntry = buildOpenAiCompatibleRegistryEntry({
|
||||
id: "cloudcode-one",
|
||||
alias: "cloudcode-one",
|
||||
baseUrl: "https://api.cloudcode.one/v1/chat/completions",
|
||||
modelsUrl: "https://api.cloudcode.one/v1/models",
|
||||
models: [
|
||||
{ id: "glm-4.7-flash", name: "GLM 4.7 Flash" },
|
||||
{ id: "glm-4.6v-flash", name: "GLM 4.6V Flash" },
|
||||
],
|
||||
passthroughModels: true,
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { RegistryEntry } from "../../shared.ts";
|
||||
import { CONOL_FALLBACK_MODELS } from "../../../../services/conolModels.ts";
|
||||
import { CONOL_FALLBACK_MODELS } from "../../../services/conolModels.ts";
|
||||
|
||||
export const conol_webProvider: RegistryEntry = {
|
||||
id: "conol-web",
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
import type { RegistryEntry } from "../../shared.ts";
|
||||
import { buildOpenAiCompatibleRegistryEntry } from "../../shared.ts";
|
||||
|
||||
/**
|
||||
* DXNT - OpenAI-compatible API with a free account quota.
|
||||
*
|
||||
* Models are discovered from the provider's authenticated catalog rather than
|
||||
* copied into a static list, so account-specific availability remains intact.
|
||||
*/
|
||||
export const dxntProvider: RegistryEntry = buildOpenAiCompatibleRegistryEntry({
|
||||
id: "dxnt",
|
||||
alias: "dxnt",
|
||||
baseUrl: "https://www.dxnt.com/v1/chat/completions",
|
||||
modelsUrl: "https://www.dxnt.com/v1/models",
|
||||
models: [],
|
||||
passthroughModels: true,
|
||||
});
|
||||
@@ -1,11 +0,0 @@
|
||||
import type { RegistryEntry } from "../../shared.ts";
|
||||
import { buildOpenAiCompatibleRegistryEntry } from "../../shared.ts";
|
||||
|
||||
export const freeAiProvider: RegistryEntry = buildOpenAiCompatibleRegistryEntry({
|
||||
id: "free-ai",
|
||||
alias: "free-ai",
|
||||
baseUrl: "https://api.free.ai/v1/chat/",
|
||||
modelsUrl: "https://api.free.ai/v1/models",
|
||||
models: [],
|
||||
passthroughModels: true,
|
||||
});
|
||||
@@ -1,11 +0,0 @@
|
||||
import type { RegistryEntry } from "../../shared.ts";
|
||||
import { buildOpenAiCompatibleRegistryEntry } from "../../shared.ts";
|
||||
|
||||
export const freeinferenceProvider: RegistryEntry = buildOpenAiCompatibleRegistryEntry({
|
||||
id: "freeinference",
|
||||
alias: "freeinference",
|
||||
baseUrl: "https://freeinference.org/v1/chat/completions",
|
||||
modelsUrl: "https://freeinference.org/v1/models",
|
||||
models: [],
|
||||
passthroughModels: true,
|
||||
});
|
||||
@@ -1,11 +0,0 @@
|
||||
import type { RegistryEntry } from "../../shared.ts";
|
||||
import { buildOpenAiCompatibleRegistryEntry } from "../../shared.ts";
|
||||
|
||||
export const helyxaiProvider: RegistryEntry = buildOpenAiCompatibleRegistryEntry({
|
||||
id: "helyxai",
|
||||
alias: "helyxai",
|
||||
baseUrl: "https://helyxai.space/v1/chat/completions",
|
||||
modelsUrl: "https://helyxai.space/v1/models",
|
||||
models: [],
|
||||
passthroughModels: true,
|
||||
});
|
||||
@@ -1,11 +0,0 @@
|
||||
import type { RegistryEntry } from "../../shared.ts";
|
||||
import { buildOpenAiCompatibleRegistryEntry } from "../../shared.ts";
|
||||
|
||||
export const literouterProvider: RegistryEntry = buildOpenAiCompatibleRegistryEntry({
|
||||
id: "literouter",
|
||||
alias: "literouter",
|
||||
baseUrl: "https://api.literouter.com/v1/chat/completions",
|
||||
modelsUrl: "https://api.literouter.com/v1/models",
|
||||
models: [],
|
||||
passthroughModels: true,
|
||||
});
|
||||
@@ -1,11 +0,0 @@
|
||||
import type { RegistryEntry } from "../../shared.ts";
|
||||
import { buildOpenAiCompatibleRegistryEntry } from "../../shared.ts";
|
||||
|
||||
export const meganovaAiProvider: RegistryEntry = buildOpenAiCompatibleRegistryEntry({
|
||||
id: "meganova-ai",
|
||||
alias: "meganova-ai",
|
||||
baseUrl: "https://api.meganova.ai/v1/chat/completions",
|
||||
modelsUrl: "https://api.meganova.ai/v1/models",
|
||||
models: [],
|
||||
passthroughModels: true,
|
||||
});
|
||||
@@ -1,11 +0,0 @@
|
||||
import type { RegistryEntry } from "../../shared.ts";
|
||||
import { buildOpenAiCompatibleRegistryEntry } from "../../shared.ts";
|
||||
|
||||
export const mixlayerProvider: RegistryEntry = buildOpenAiCompatibleRegistryEntry({
|
||||
id: "mixlayer",
|
||||
alias: "mixlayer",
|
||||
baseUrl: "https://models.mixlayer.ai/v1/chat/completions",
|
||||
modelsUrl: "https://models.mixlayer.ai/v1/models",
|
||||
models: [{ id: "qwen/qwen3.5-4b-free", name: "Qwen 3.5 4B (free)" }],
|
||||
passthroughModels: true,
|
||||
});
|
||||
@@ -1,11 +0,0 @@
|
||||
import type { RegistryEntry } from "../../shared.ts";
|
||||
import { buildOpenAiCompatibleRegistryEntry } from "../../shared.ts";
|
||||
|
||||
export const mnnAiProvider: RegistryEntry = buildOpenAiCompatibleRegistryEntry({
|
||||
id: "mnn-ai",
|
||||
alias: "mnn-ai",
|
||||
baseUrl: "https://api.mnnai.ru/v1/chat/completions",
|
||||
modelsUrl: "https://api.mnnai.ru/v1/models",
|
||||
models: [],
|
||||
passthroughModels: true,
|
||||
});
|
||||
@@ -1,12 +0,0 @@
|
||||
import type { RegistryEntry } from "../../shared.ts";
|
||||
import { buildOpenAiCompatibleRegistryEntry } from "../../shared.ts";
|
||||
|
||||
// Free access terms may permit data collection or training use; discover models dynamically.
|
||||
export const nagaAiProvider: RegistryEntry = buildOpenAiCompatibleRegistryEntry({
|
||||
id: "naga-ai",
|
||||
alias: "naga-ai",
|
||||
baseUrl: "https://api.naga.ac/v1/chat/completions",
|
||||
modelsUrl: "https://api.naga.ac/v1/models",
|
||||
models: [],
|
||||
passthroughModels: true,
|
||||
});
|
||||
@@ -1,11 +0,0 @@
|
||||
import type { RegistryEntry } from "../../shared.ts";
|
||||
import { buildOpenAiCompatibleRegistryEntry } from "../../shared.ts";
|
||||
|
||||
export const ofoxaiProvider: RegistryEntry = buildOpenAiCompatibleRegistryEntry({
|
||||
id: "ofoxai",
|
||||
alias: "ofoxai",
|
||||
baseUrl: "https://api.ofox.ai/v1/chat/completions",
|
||||
modelsUrl: "https://api.ofox.ai/v1/models",
|
||||
models: [],
|
||||
passthroughModels: true,
|
||||
});
|
||||
@@ -1,11 +0,0 @@
|
||||
import type { RegistryEntry } from "../../shared.ts";
|
||||
import { buildOpenAiCompatibleRegistryEntry } from "../../shared.ts";
|
||||
|
||||
export const poixeAiProvider: RegistryEntry = buildOpenAiCompatibleRegistryEntry({
|
||||
id: "poixe-ai",
|
||||
alias: "poixe-ai",
|
||||
baseUrl: "https://api.poixe.com/v1/chat/completions",
|
||||
modelsUrl: "https://api.poixe.com/v1/models",
|
||||
models: [],
|
||||
passthroughModels: true,
|
||||
});
|
||||
@@ -1,11 +0,0 @@
|
||||
import type { RegistryEntry } from "../../shared.ts";
|
||||
import { buildOpenAiCompatibleRegistryEntry } from "../../shared.ts";
|
||||
|
||||
export const spekaProvider: RegistryEntry = buildOpenAiCompatibleRegistryEntry({
|
||||
id: "speka",
|
||||
alias: "speka",
|
||||
baseUrl: "https://speka.me/v1/chat/completions",
|
||||
modelsUrl: "https://speka.me/v1/models",
|
||||
models: [],
|
||||
passthroughModels: true,
|
||||
});
|
||||
@@ -1,11 +0,0 @@
|
||||
import type { RegistryEntry } from "../../shared.ts";
|
||||
import { buildOpenAiCompatibleRegistryEntry } from "../../shared.ts";
|
||||
|
||||
export const tokenreplyProvider: RegistryEntry = buildOpenAiCompatibleRegistryEntry({
|
||||
id: "tokenreply",
|
||||
alias: "tokenreply",
|
||||
baseUrl: "https://api.tokenreply.com/v1/chat/completions",
|
||||
modelsUrl: "https://api.tokenreply.com/v1/models",
|
||||
models: [],
|
||||
passthroughModels: true,
|
||||
});
|
||||
@@ -1,11 +1,14 @@
|
||||
import type { RegistryEntry } from "../../shared.ts";
|
||||
import { buildOpenAiCompatibleRegistryEntry } from "../../shared.ts";
|
||||
|
||||
export const unorouterProvider: RegistryEntry = buildOpenAiCompatibleRegistryEntry({
|
||||
export const unorouterProvider: RegistryEntry = {
|
||||
id: "unorouter",
|
||||
alias: "unorouter",
|
||||
baseUrl: "https://api.unorouter.com/v1/chat/completions",
|
||||
modelsUrl: "https://api.unorouter.com/v1/models",
|
||||
models: [],
|
||||
format: "openai",
|
||||
executor: "default",
|
||||
baseUrl: "https://api.unorouter.ai/v1/chat/completions",
|
||||
authType: "apikey",
|
||||
authHeader: "bearer",
|
||||
defaultContextLength: 128000,
|
||||
models: [{ id: "auto", name: "Auto (Best Available)" }],
|
||||
passthroughModels: true,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
import type { RegistryEntry } from "../../shared.ts";
|
||||
import { buildOpenAiCompatibleRegistryEntry } from "../../shared.ts";
|
||||
|
||||
/**
|
||||
* Yolo-Auto - OpenAI-compatible API with a request-limited free tier.
|
||||
*
|
||||
* The catalog is kept intentionally small: the documented free-tier model is
|
||||
* seeded while passthrough discovery allows the service to publish updates.
|
||||
*/
|
||||
export const yoloAutoProvider: RegistryEntry = buildOpenAiCompatibleRegistryEntry({
|
||||
id: "yolo-auto",
|
||||
alias: "yolo-auto",
|
||||
baseUrl: "https://yolo-auto.com/v1/chat/completions",
|
||||
modelsUrl: "https://yolo-auto.com/v1/models",
|
||||
models: [{ id: "qwen3.6-35b-a3b", name: "Qwen 3.6 35B A3B" }],
|
||||
passthroughModels: true,
|
||||
});
|
||||
@@ -1,11 +0,0 @@
|
||||
import type { RegistryEntry } from "../../shared.ts";
|
||||
import { buildOpenAiCompatibleRegistryEntry } from "../../shared.ts";
|
||||
|
||||
export const zerolimitaiProvider: RegistryEntry = buildOpenAiCompatibleRegistryEntry({
|
||||
id: "zerolimitai",
|
||||
alias: "zerolimitai",
|
||||
baseUrl: "https://www.zerolimitai.com/api/v1/chat/completions",
|
||||
modelsUrl: "https://www.zerolimitai.com/api/v1/models",
|
||||
models: [],
|
||||
passthroughModels: true,
|
||||
});
|
||||
@@ -1,5 +1,3 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
|
||||
import { BaseExecutor, type ExecuteInput } from "./base.ts";
|
||||
import { makeExecutorErrorResult as makeErrorResult } from "../utils/error.ts";
|
||||
import { initTinyCmsWasm, generateSecurePayload } from "./tinycmsSigner.ts";
|
||||
@@ -30,9 +28,9 @@ async function fetchChallenge(uuid: string): Promise<any> {
|
||||
const res = await fetch(CHALLENGE_URL, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
uuid: uuid,
|
||||
"uuid": uuid,
|
||||
"x-origin": "https://gov.freegpt.win",
|
||||
Accept: "application/json",
|
||||
"Accept": "application/json",
|
||||
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36",
|
||||
},
|
||||
});
|
||||
@@ -69,10 +67,10 @@ export class TinyCmsExecutor extends BaseExecutor {
|
||||
const challengeObj = await fetchChallenge(uuid);
|
||||
|
||||
const timestamp = Date.now().toString();
|
||||
// The nonce is signed into the anti-replay payload below, so it must come from a CSPRNG.
|
||||
// `randomUUID` from node:crypto is always available on the supported runtimes — the old
|
||||
// non-cryptographic fallback produced a predictable nonce with no way to notice.
|
||||
const nonceJs = randomUUID();
|
||||
const nonceJs =
|
||||
typeof crypto !== "undefined" && crypto.randomUUID
|
||||
? crypto.randomUUID()
|
||||
: `${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
||||
|
||||
const securePayload = generateSecurePayload(
|
||||
uuid,
|
||||
@@ -123,7 +121,12 @@ export class TinyCmsExecutor extends BaseExecutor {
|
||||
body: response.body,
|
||||
};
|
||||
} catch (err: any) {
|
||||
return makeErrorResult(500, `TinyCMS Error: ${err.message}`, body, CHAT_URL);
|
||||
return makeErrorResult(
|
||||
500,
|
||||
`TinyCMS Error: ${err.message}`,
|
||||
body,
|
||||
CHAT_URL
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -251,10 +251,7 @@ import { recordCompressionCacheStats } from "./chatCore/compressionCacheStats.ts
|
||||
import { writeCavemanOutputAnalytics } from "./chatCore/cavemanOutputAnalytics.ts";
|
||||
import { scheduleQuotaShareConsumption } from "./chatCore/quotaShareConsumption.ts";
|
||||
import { emitRequestGamificationEvent } from "./chatCore/gamificationEvent.ts";
|
||||
import {
|
||||
runPluginOnResponseHook,
|
||||
runPluginOnStreamCompleteHook,
|
||||
} from "./chatCore/pluginOnResponse.ts";
|
||||
import { runPluginOnResponseHook } from "./chatCore/pluginOnResponse.ts";
|
||||
import { scheduleStreamingQuotaShareConsumption } from "./chatCore/streamingQuotaShare.ts";
|
||||
import { recordStreamingUsageStats } from "./chatCore/streamingUsageStats.ts";
|
||||
import { recordStreamingCost } from "./chatCore/streamingCost.ts";
|
||||
@@ -4937,17 +4934,6 @@ export async function handleChatCore({
|
||||
streamUsage,
|
||||
log,
|
||||
});
|
||||
|
||||
// Plugin onStreamComplete hook — fire-and-forget, fail-open (#9571)
|
||||
runPluginOnStreamCompleteHook({
|
||||
status: normalizedStreamStatus,
|
||||
usage: streamUsage as Record<string, unknown> | undefined,
|
||||
ttft,
|
||||
model,
|
||||
provider,
|
||||
errorCode: streamErrorCode,
|
||||
startTime,
|
||||
});
|
||||
};
|
||||
|
||||
const streamFailureFinalizers = streamFailure.createStreamFailureFinalizers({
|
||||
|
||||
@@ -45,57 +45,3 @@ export async function runPluginOnResponseHook(args: {
|
||||
/* plugin onResponse optional */
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Payload passed to plugin onStreamComplete hooks after a streaming response is consumed.
|
||||
* Carries usage token counts, timing metrics (latency, TTFT), model, provider, and error code.
|
||||
*/
|
||||
export type PluginOnStreamCompletePayload = {
|
||||
status: number;
|
||||
usage?: {
|
||||
prompt_tokens?: number;
|
||||
completion_tokens?: number;
|
||||
reasoning_tokens?: number;
|
||||
cache_read_input_tokens?: number;
|
||||
cache_creation_input_tokens?: number;
|
||||
};
|
||||
timing?: {
|
||||
latencyMs: number;
|
||||
ttft?: number;
|
||||
};
|
||||
model?: string;
|
||||
provider?: string;
|
||||
errorCode?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Run plugin onStreamComplete hooks — fire-and-forget and fail-open.
|
||||
* Called inside the onStreamComplete callback (chatCore.ts) where usage and timing data
|
||||
* converge after an SSE stream is fully consumed.
|
||||
*/
|
||||
export async function runPluginOnStreamCompleteHook(args: {
|
||||
status: number;
|
||||
usage?: Record<string, unknown>;
|
||||
ttft?: number;
|
||||
model: string | null | undefined;
|
||||
provider: string | null | undefined;
|
||||
errorCode?: string | null | undefined;
|
||||
startTime: number;
|
||||
}): Promise<void> {
|
||||
try {
|
||||
const { runOnStreamComplete } = await import("@/lib/plugins/hooks");
|
||||
runOnStreamComplete({
|
||||
status: args.status,
|
||||
usage: args.usage as PluginOnStreamCompletePayload["usage"],
|
||||
timing: {
|
||||
latencyMs: Date.now() - args.startTime,
|
||||
ttft: args.ttft,
|
||||
},
|
||||
model: args.model ?? undefined,
|
||||
provider: args.provider ?? undefined,
|
||||
errorCode: args.errorCode ?? undefined,
|
||||
}).catch(() => {});
|
||||
} catch (_) {
|
||||
/* plugin onStreamComplete optional */
|
||||
}
|
||||
}
|
||||
|
||||
@@ -237,16 +237,12 @@ function trustedEnvironmentText(parsed: CodexParsedRequest): string {
|
||||
}
|
||||
|
||||
function decodeXmlText(value: string): string {
|
||||
// `&` MUST be decoded last: decoding it first turns `&quot;` (the encoding of the
|
||||
// literal text `"`) into `"`, which the following pass then decodes again into `"`.
|
||||
// These values feed the workspace-root trust comparison below, so a double-unescape lets an
|
||||
// encoded path decode into a different path than the one the client actually declared.
|
||||
return value
|
||||
.replaceAll("<", "<")
|
||||
.replaceAll(">", ">")
|
||||
.replaceAll("&", "&")
|
||||
.replaceAll(""", '"')
|
||||
.replaceAll("'", "'")
|
||||
.replaceAll("&", "&");
|
||||
.replaceAll("'", "'");
|
||||
}
|
||||
|
||||
function uniqueAbsolutePaths(values: string[], field: string): string[] {
|
||||
|
||||
@@ -1,237 +0,0 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* Conductor panel (PRD Conductor RF3): fleet + task queue live view over the
|
||||
* /api/conductor proxy routes. The browser never talks to the hub — everything
|
||||
* goes through the server-side proxy (hub token stays in server env).
|
||||
*/
|
||||
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
import { Badge, Card, ConfirmModal, DataTable, EmptyState, Modal } from "@/shared/components";
|
||||
|
||||
import FaroChat from "./FaroChat";
|
||||
|
||||
interface FleetRunner {
|
||||
id: string;
|
||||
name: string;
|
||||
clis: string[];
|
||||
online: boolean;
|
||||
draining: boolean;
|
||||
}
|
||||
|
||||
interface FleetTask {
|
||||
id: string;
|
||||
status: string;
|
||||
mode: string;
|
||||
repo: string | null;
|
||||
runner: string | null;
|
||||
summary: string | null;
|
||||
branch: string | null;
|
||||
error: string | null;
|
||||
updated_at: string | null;
|
||||
}
|
||||
|
||||
interface FleetSnapshot {
|
||||
offline: boolean;
|
||||
runners: FleetRunner[];
|
||||
tasks: FleetTask[];
|
||||
}
|
||||
|
||||
interface TaskDetail extends FleetTask {
|
||||
prompt: string | null;
|
||||
base_ref: string | null;
|
||||
council: { candidate_task_ids?: string[] } | null;
|
||||
}
|
||||
|
||||
const REFRESH_MS = 5000;
|
||||
const TERMINAL = new Set(["completed", "failed", "canceled"]);
|
||||
|
||||
function statusVariant(status: string): "success" | "error" | "warning" | "info" | "default" {
|
||||
if (status === "completed") return "success";
|
||||
if (status === "failed") return "error";
|
||||
if (status === "canceled" || status === "input_required") return "warning";
|
||||
if (status === "working") return "info";
|
||||
return "default";
|
||||
}
|
||||
|
||||
export default function ConductorPageClient() {
|
||||
const t = useTranslations("conductor");
|
||||
const [snapshot, setSnapshot] = useState<FleetSnapshot | null>(null);
|
||||
const [detail, setDetail] = useState<TaskDetail | null>(null);
|
||||
const [cancelTarget, setCancelTarget] = useState<string | null>(null);
|
||||
const [canceling, setCanceling] = useState(false);
|
||||
const [err, setErr] = useState("");
|
||||
|
||||
const load = useCallback(async () => {
|
||||
try {
|
||||
const res = await fetch("/api/conductor/fleet");
|
||||
if (res.ok) setSnapshot(await res.json());
|
||||
} catch {
|
||||
// rede local instável: mantém o último snapshot; o banner offline vem do servidor
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
void load();
|
||||
const timer = setInterval(() => void load(), REFRESH_MS);
|
||||
return () => clearInterval(timer);
|
||||
}, [load]);
|
||||
|
||||
const openDetail = async (taskId: string) => {
|
||||
setErr("");
|
||||
try {
|
||||
const res = await fetch(`/api/conductor/tasks/${encodeURIComponent(taskId)}`);
|
||||
if (res.ok) setDetail(await res.json());
|
||||
else setErr(`${t("error")}: HTTP ${res.status}`);
|
||||
} catch {
|
||||
setErr(t("error"));
|
||||
}
|
||||
};
|
||||
|
||||
const confirmCancel = async () => {
|
||||
if (!cancelTarget) return;
|
||||
setCanceling(true);
|
||||
setErr("");
|
||||
try {
|
||||
const res = await fetch(`/api/conductor/tasks/${encodeURIComponent(cancelTarget)}/cancel`, { method: "POST" });
|
||||
if (!res.ok) setErr(`${t("cancelFailed")} (HTTP ${res.status})`);
|
||||
else {
|
||||
setDetail(null);
|
||||
await load();
|
||||
}
|
||||
} catch {
|
||||
setErr(t("cancelFailed"));
|
||||
} finally {
|
||||
setCanceling(false);
|
||||
setCancelTarget(null);
|
||||
}
|
||||
};
|
||||
|
||||
const runners = snapshot?.runners ?? [];
|
||||
const tasks = snapshot?.tasks ?? [];
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h1 className="text-xl font-semibold">{t("title")}</h1>
|
||||
<p className="text-sm text-text-muted">{t("subtitle")}</p>
|
||||
</div>
|
||||
|
||||
{err && <Badge variant="error">{err}</Badge>}
|
||||
|
||||
{snapshot?.offline ? (
|
||||
<Card>
|
||||
<EmptyState icon="cloud_off" title={t("hubOffline")} />
|
||||
</Card>
|
||||
) : (
|
||||
<>
|
||||
<Card title={t("runners")}>
|
||||
<DataTable
|
||||
columns={[
|
||||
{ key: "name", label: t("colName") },
|
||||
{ key: "clis", label: t("colClis") },
|
||||
{ key: "status", label: t("colStatus") },
|
||||
]}
|
||||
data={runners.map((r) => ({ ...r, id: r.id }))}
|
||||
emptyMessage={t("noRunners")}
|
||||
loading={snapshot === null}
|
||||
renderCell={(row, column) => {
|
||||
const r = row as unknown as FleetRunner;
|
||||
if (column.key === "name") return <span className="font-medium">{r.name}</span>;
|
||||
if (column.key === "clis") return r.clis.join(" / ");
|
||||
if (r.draining) return <Badge variant="warning" dot>{t("draining")}</Badge>;
|
||||
return r.online ? (
|
||||
<Badge variant="success" dot>{t("online")}</Badge>
|
||||
) : (
|
||||
<Badge variant="error" dot>{t("offline")}</Badge>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Card title={t("tasks")}>
|
||||
<DataTable
|
||||
columns={[
|
||||
{ key: "id", label: t("colTask") },
|
||||
{ key: "status", label: t("colStatus") },
|
||||
{ key: "mode", label: t("colMode") },
|
||||
{ key: "runner", label: t("colRunner") },
|
||||
{ key: "summary", label: t("colSummary"), maxWidth: "28rem" },
|
||||
]}
|
||||
data={tasks.map((task) => ({ ...task, id: task.id }))}
|
||||
emptyMessage={t("noTasks")}
|
||||
loading={snapshot === null}
|
||||
onRowClick={(row) => void openDetail(String(row.id))}
|
||||
renderCell={(row, column) => {
|
||||
const task = row as unknown as FleetTask;
|
||||
if (column.key === "status") return <Badge variant={statusVariant(task.status)} dot>{task.status}</Badge>;
|
||||
if (column.key === "id") return <code className="text-xs">{task.id}</code>;
|
||||
if (column.key === "summary") return task.summary ?? task.error ?? "—";
|
||||
return (task as unknown as Record<string, unknown>)[column.key]?.toString() ?? "—";
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
</>
|
||||
)}
|
||||
|
||||
<FaroChat />
|
||||
|
||||
<Modal isOpen={detail !== null} onClose={() => setDetail(null)} title={t("detailTitle")} size="lg">
|
||||
{detail && (
|
||||
<div className="space-y-4 text-sm">
|
||||
<div className="flex items-center gap-2">
|
||||
<code className="text-xs">{detail.id}</code>
|
||||
<Badge variant={statusVariant(detail.status)} dot>{detail.status}</Badge>
|
||||
<Badge>{detail.mode}</Badge>
|
||||
{detail.runner && <Badge variant="info">{detail.runner}</Badge>}
|
||||
</div>
|
||||
{detail.prompt && (
|
||||
<div>
|
||||
<div className="font-medium">{t("prompt")}</div>
|
||||
<pre className="whitespace-pre-wrap text-xs bg-black/5 dark:bg-white/5 rounded p-2">{detail.prompt}</pre>
|
||||
</div>
|
||||
)}
|
||||
{detail.summary && <p>{detail.summary}</p>}
|
||||
{detail.error && <Badge variant="error">{detail.error}</Badge>}
|
||||
{detail.branch && (
|
||||
<div>
|
||||
<div className="font-medium">{t("branch")}</div>
|
||||
<code className="text-xs">{detail.branch}</code>
|
||||
<p className="text-xs text-text-muted">{t("fetchHint", { branch: detail.branch })}</p>
|
||||
</div>
|
||||
)}
|
||||
{detail.mode.startsWith("council") && detail.council?.candidate_task_ids && (
|
||||
<div>
|
||||
<div className="font-medium">{t("council")}</div>
|
||||
<p className="text-xs">
|
||||
{t("candidates")}: {detail.council.candidate_task_ids.join(", ")}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{!TERMINAL.has(detail.status) && (
|
||||
<button
|
||||
type="button"
|
||||
className="text-sm text-red-600 dark:text-red-400 underline"
|
||||
onClick={() => setCancelTarget(detail.id)}
|
||||
>
|
||||
{t("cancel")}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
|
||||
<ConfirmModal
|
||||
isOpen={cancelTarget !== null}
|
||||
onClose={() => setCancelTarget(null)}
|
||||
onConfirm={confirmCancel}
|
||||
title={t("cancelConfirmTitle")}
|
||||
message={t("cancelConfirmMessage")}
|
||||
confirmText={t("cancel")}
|
||||
loading={canceling}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,272 +0,0 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* Faro chat with voice (Conductor PRD RF4). Text: input → /api/conductor/ask
|
||||
* (server-side proxy — the hub credential never reaches the browser). When the
|
||||
* answer carries `pending`, Faro is asking for confirmation: the Sim/Não
|
||||
* buttons just send "sim"/"não" — the safety gate lives in Faro's engine.
|
||||
*
|
||||
* Voice (guaranteed cycle, PRD RF4): push-to-talk → MediaRecorder →
|
||||
* POST /api/v1/audio/transcriptions (multipart) → text → /ask → response →
|
||||
* POST /api/v1/audio/speech → play the returned audio blob. STT/TTS models are
|
||||
* operator-configurable (provider/model of THIS OmniRoute install), persisted
|
||||
* in localStorage.
|
||||
*/
|
||||
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
import { Badge, Card } from "@/shared/components";
|
||||
|
||||
interface ChatMessage {
|
||||
role: "user" | "faro";
|
||||
text: string;
|
||||
}
|
||||
|
||||
type VoiceState = "idle" | "listening" | "thinking" | "speaking";
|
||||
|
||||
const STT_KEY = "conductor.sttModel";
|
||||
const TTS_KEY = "conductor.ttsModel";
|
||||
|
||||
function safeGet(key: string, fallback: string): string {
|
||||
try {
|
||||
return localStorage.getItem(key) || fallback;
|
||||
} catch {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
async function errorMessageOf(res: Response, fallback: string): Promise<string> {
|
||||
try {
|
||||
const data = await res.json();
|
||||
return data?.error?.message ?? fallback;
|
||||
} catch {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
export default function FaroChat() {
|
||||
const t = useTranslations("conductor");
|
||||
const [messages, setMessages] = useState<ChatMessage[]>([]);
|
||||
const [input, setInput] = useState("");
|
||||
const [pending, setPending] = useState(false);
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [voice, setVoice] = useState<VoiceState>("idle");
|
||||
const [speak, setSpeak] = useState(false);
|
||||
const [sttModel, setSttModel] = useState("openai/whisper-1");
|
||||
const [ttsModel, setTtsModel] = useState("openai/tts-1");
|
||||
const [err, setErr] = useState("");
|
||||
const recorderRef = useRef<MediaRecorder | null>(null);
|
||||
const logRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
setSttModel(safeGet(STT_KEY, "openai/whisper-1"));
|
||||
setTtsModel(safeGet(TTS_KEY, "openai/tts-1"));
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
logRef.current?.scrollTo({ top: logRef.current.scrollHeight });
|
||||
}, [messages]);
|
||||
|
||||
const persistModels = (stt: string, tts: string) => {
|
||||
setSttModel(stt);
|
||||
setTtsModel(tts);
|
||||
try {
|
||||
localStorage.setItem(STT_KEY, stt);
|
||||
localStorage.setItem(TTS_KEY, tts);
|
||||
} catch {
|
||||
// modo privado: segue só em memória
|
||||
}
|
||||
};
|
||||
|
||||
const playAnswer = async (text: string) => {
|
||||
setVoice("speaking");
|
||||
try {
|
||||
const res = await fetch("/api/v1/audio/speech", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ model: ttsModel, input: text }),
|
||||
});
|
||||
if (!res.ok) {
|
||||
setErr(await errorMessageOf(res, t("ttsFailed")));
|
||||
return;
|
||||
}
|
||||
const url = URL.createObjectURL(await res.blob());
|
||||
const audio = new Audio(url);
|
||||
await audio.play().catch(() => undefined);
|
||||
audio.onended = () => URL.revokeObjectURL(url);
|
||||
} finally {
|
||||
setVoice("idle");
|
||||
}
|
||||
};
|
||||
|
||||
const send = async (message: string, viaVoice = false) => {
|
||||
const clean = message.trim();
|
||||
if (!clean || busy) return;
|
||||
setErr("");
|
||||
setBusy(true);
|
||||
setVoice("thinking");
|
||||
setMessages((m) => [...m, { role: "user", text: clean }]);
|
||||
setInput("");
|
||||
try {
|
||||
const res = await fetch("/api/conductor/ask", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ message: clean }),
|
||||
});
|
||||
if (!res.ok) {
|
||||
setErr(await errorMessageOf(res, t("faroOffline")));
|
||||
return;
|
||||
}
|
||||
const data = await res.json();
|
||||
setMessages((m) => [...m, { role: "faro", text: data.text }]);
|
||||
setPending(Boolean(data.pending));
|
||||
if (viaVoice && speak && data.text) await playAnswer(data.text);
|
||||
} catch {
|
||||
setErr(t("faroOffline"));
|
||||
} finally {
|
||||
setBusy(false);
|
||||
setVoice((v) => (v === "thinking" ? "idle" : v));
|
||||
}
|
||||
};
|
||||
|
||||
const startRecording = async () => {
|
||||
setErr("");
|
||||
try {
|
||||
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||
const recorder = new MediaRecorder(stream);
|
||||
const chunks: Blob[] = [];
|
||||
recorder.ondataavailable = (e) => e.data.size > 0 && chunks.push(e.data);
|
||||
recorder.onstop = async () => {
|
||||
stream.getTracks().forEach((track) => track.stop());
|
||||
setVoice("thinking");
|
||||
const blob = new Blob(chunks, { type: recorder.mimeType || "audio/webm" });
|
||||
const form = new FormData();
|
||||
form.append("model", sttModel);
|
||||
form.append("file", new File([blob], "faro-ptt.webm", { type: blob.type }));
|
||||
try {
|
||||
// multipart: sem header manual — o browser define o boundary
|
||||
const res = await fetch("/api/v1/audio/transcriptions", { method: "POST", body: form });
|
||||
if (!res.ok) {
|
||||
setErr(await errorMessageOf(res, t("sttFailed")));
|
||||
setVoice("idle");
|
||||
return;
|
||||
}
|
||||
const data = await res.json();
|
||||
if (data.text) await send(data.text, true);
|
||||
else setVoice("idle");
|
||||
} catch {
|
||||
setErr(t("sttFailed"));
|
||||
setVoice("idle");
|
||||
}
|
||||
};
|
||||
recorderRef.current = recorder;
|
||||
recorder.start();
|
||||
setVoice("listening");
|
||||
} catch {
|
||||
setErr(t("micDenied"));
|
||||
}
|
||||
};
|
||||
|
||||
const stopRecording = () => {
|
||||
if (recorderRef.current?.state === "recording") recorderRef.current.stop();
|
||||
recorderRef.current = null;
|
||||
};
|
||||
|
||||
const voiceLabel: Record<VoiceState, string> = {
|
||||
idle: t("voiceIdle"),
|
||||
listening: t("voiceListening"),
|
||||
thinking: t("voiceThinking"),
|
||||
speaking: t("voiceSpeaking"),
|
||||
};
|
||||
|
||||
return (
|
||||
<Card title={t("faroTitle")} subtitle={t("faroSubtitle")}>
|
||||
<div className="space-y-3">
|
||||
<div ref={logRef} className="max-h-72 overflow-y-auto space-y-2 text-sm">
|
||||
{messages.length === 0 && <p className="text-text-muted text-xs">{t("faroEmpty")}</p>}
|
||||
{messages.map((m, i) => (
|
||||
<div key={i} className={m.role === "user" ? "text-right" : "text-left"}>
|
||||
<span
|
||||
className={
|
||||
m.role === "user"
|
||||
? "inline-block rounded px-2 py-1 bg-primary/10"
|
||||
: "inline-block rounded px-2 py-1 bg-black/5 dark:bg-white/10 whitespace-pre-wrap"
|
||||
}
|
||||
>
|
||||
{m.text}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{err && <Badge variant="error">{err}</Badge>}
|
||||
{pending && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="warning" dot>{t("faroPending")}</Badge>
|
||||
<button type="button" className="text-sm underline" onClick={() => void send("sim")}>
|
||||
{t("yes")}
|
||||
</button>
|
||||
<button type="button" className="text-sm underline" onClick={() => void send("não")}>
|
||||
{t("no")}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
className="flex-1 rounded border border-black/10 dark:border-white/10 bg-transparent px-2 py-1 text-sm"
|
||||
placeholder={t("faroPlaceholder")}
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") void send(input);
|
||||
}}
|
||||
disabled={busy}
|
||||
/>
|
||||
<button type="button" className="text-sm underline" onClick={() => void send(input)} disabled={busy}>
|
||||
{t("faroSend")}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`text-sm px-2 py-1 rounded ${voice === "listening" ? "bg-red-500/20" : "bg-black/5 dark:bg-white/10"}`}
|
||||
title={t("pushToTalk")}
|
||||
aria-pressed={voice === "listening"}
|
||||
onMouseDown={() => void startRecording()}
|
||||
onMouseUp={stopRecording}
|
||||
onMouseLeave={stopRecording}
|
||||
onTouchStart={() => void startRecording()}
|
||||
onTouchEnd={stopRecording}
|
||||
>
|
||||
🎙 {voiceLabel[voice]}
|
||||
</button>
|
||||
<label className="flex items-center gap-1 text-xs text-text-muted">
|
||||
<input type="checkbox" checked={speak} onChange={(e) => setSpeak(e.target.checked)} />
|
||||
{t("speakAnswers")}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<details className="text-xs text-text-muted">
|
||||
<summary>{t("voiceModels")}</summary>
|
||||
<div className="flex gap-2 pt-2">
|
||||
<label className="flex-1">
|
||||
STT
|
||||
<input
|
||||
className="w-full rounded border border-black/10 dark:border-white/10 bg-transparent px-2 py-1"
|
||||
value={sttModel}
|
||||
onChange={(e) => persistModels(e.target.value, ttsModel)}
|
||||
/>
|
||||
</label>
|
||||
<label className="flex-1">
|
||||
TTS
|
||||
<input
|
||||
className="w-full rounded border border-black/10 dark:border-white/10 bg-transparent px-2 py-1"
|
||||
value={ttsModel}
|
||||
onChange={(e) => persistModels(sttModel, e.target.value)}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
import ConductorPageClient from "./ConductorPageClient";
|
||||
|
||||
export const metadata = {
|
||||
title: "Conductor — OmniRoute",
|
||||
description: "OmniConductor CLI-agent fleet: runners, task queue and councils, live.",
|
||||
};
|
||||
|
||||
export default function ConductorPage() {
|
||||
return <ConductorPageClient />;
|
||||
}
|
||||
@@ -1,9 +1,5 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { z } from "zod";
|
||||
|
||||
import { getTaskManager, type TaskState } from "@/lib/a2a/taskManager";
|
||||
import { createConductorTask } from "@/lib/conductor/hubProxy";
|
||||
import { getSettings } from "@/lib/db/settings";
|
||||
|
||||
const VALID_TASK_STATES = new Set<TaskState>([
|
||||
"submitted",
|
||||
@@ -48,97 +44,3 @@ export async function GET(request: Request) {
|
||||
return NextResponse.json({ error: message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// ============ POST — delegação de entrada à frota do Conductor (PRD Conductor RF5) ============
|
||||
|
||||
const delegationSchema = z.object({
|
||||
skill: z.string().default("conductor"),
|
||||
messages: z.array(z.object({ role: z.string(), content: z.string() })).min(1),
|
||||
metadata: z
|
||||
.object({
|
||||
conductor: z
|
||||
.object({
|
||||
repo: z.object({ url: z.string().min(1), base_ref: z.string().optional() }).optional(),
|
||||
mode: z.string().optional(),
|
||||
cli: z.string().optional(),
|
||||
model: z.string().optional(),
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
.optional(),
|
||||
});
|
||||
|
||||
/** Mesma semântica de auth do JSON-RPC A2A (src/app/a2a/route.ts): Bearer vs OMNIROUTE_API_KEY; aberto se não configurada. */
|
||||
function authenticateA2A(request: Request): boolean {
|
||||
const configuredKey = process.env.OMNIROUTE_API_KEY;
|
||||
if (!configuredKey) return true;
|
||||
const token = (request.headers.get("authorization") || "").replace(/^Bearer\s+/i, "");
|
||||
return token === configuredKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Traduz uma task A2A externa em `POST /v1/tasks` do hub do OmniConductor.
|
||||
* Só skills da frota (`conductor` / `conductor-cli-<profile>` — as anunciadas no
|
||||
* Agent Card) são delegáveis; os estados voltam pelo espelho SSE→A2A (RF1).
|
||||
*/
|
||||
export async function POST(request: Request) {
|
||||
if (!authenticateA2A(request)) {
|
||||
return NextResponse.json({ error: "Unauthorized: missing or invalid API key" }, { status: 401 });
|
||||
}
|
||||
const settings = await getSettings();
|
||||
if (settings.a2aEnabled !== true) {
|
||||
return NextResponse.json(
|
||||
{ error: "A2A endpoint is disabled. Enable it from the Endpoints page." },
|
||||
{ status: 503 }
|
||||
);
|
||||
}
|
||||
|
||||
let raw: unknown;
|
||||
try {
|
||||
raw = await request.json();
|
||||
} catch {
|
||||
return NextResponse.json({ error: "Invalid JSON body" }, { status: 400 });
|
||||
}
|
||||
const parsed = delegationSchema.safeParse(raw);
|
||||
if (!parsed.success) {
|
||||
return NextResponse.json({ error: "Invalid A2A task: provide messages[] (and metadata.conductor)" }, { status: 400 });
|
||||
}
|
||||
const { skill, messages, metadata } = parsed.data;
|
||||
if (skill !== "conductor" && !skill.startsWith("conductor-cli-")) {
|
||||
return NextResponse.json(
|
||||
{ error: "Only Conductor fleet skills are delegable here (conductor / conductor-cli-<profile>)" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
const conductor = metadata?.conductor;
|
||||
if (!conductor?.repo?.url) {
|
||||
return NextResponse.json(
|
||||
{ error: "Delegation requires metadata.conductor.repo.url (the fleet works on git repos)" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
const prompt = [...messages].reverse().find((m) => m.role === "user")?.content ?? messages[messages.length - 1].content;
|
||||
|
||||
const created = await createConductorTask({
|
||||
repoUrl: conductor.repo.url,
|
||||
baseRef: conductor.repo.base_ref,
|
||||
prompt,
|
||||
mode: conductor.mode,
|
||||
cli: skill.startsWith("conductor-cli-") ? skill.slice("conductor-cli-".length) : conductor.cli,
|
||||
model: conductor.model,
|
||||
});
|
||||
if (!created.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: `Conductor hub refused the delegation (HTTP ${created.status})` },
|
||||
{ status: created.status }
|
||||
);
|
||||
}
|
||||
return NextResponse.json(
|
||||
{
|
||||
conductor_task_id: created.task_id,
|
||||
state: "submitted",
|
||||
note: "States flow back through the SSE→A2A mirror (GET /api/a2a/tasks, skill=conductor).",
|
||||
},
|
||||
{ status: 201 }
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
/**
|
||||
* POST /api/conductor/ask — proxy para o Faro (spokesperson do OmniConductor).
|
||||
* O /ask do Faro exige credencial do hub (server-side); o browser fala só com
|
||||
* esta rota. Resposta whitelisted {text, pending} — quando `pending` vier, a UI
|
||||
* oferece Sim/Não (a trava de confirmação é do motor do Faro; nunca contornada).
|
||||
*/
|
||||
|
||||
import { NextResponse } from "next/server";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createErrorResponse } from "@/lib/api/errorResponse";
|
||||
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
|
||||
import { askFaro } from "@/lib/conductor/faroProxy";
|
||||
|
||||
const askSchema = z.object({ message: z.string().min(1).max(4000) });
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const authError = await requireManagementAuth(request);
|
||||
if (authError) return authError;
|
||||
|
||||
let raw: unknown;
|
||||
try {
|
||||
raw = await request.json();
|
||||
} catch {
|
||||
return createErrorResponse({ status: 400, message: "Invalid JSON body" });
|
||||
}
|
||||
const parsed = askSchema.safeParse(raw);
|
||||
if (!parsed.success) {
|
||||
return createErrorResponse({ status: 400, message: "Body must be { message: string (1-4000 chars) }" });
|
||||
}
|
||||
|
||||
const answer = await askFaro(parsed.data.message);
|
||||
if (!answer.ok) {
|
||||
return createErrorResponse({ status: 503, message: "Faro (spokesperson) is offline or refused the request" });
|
||||
}
|
||||
return NextResponse.json({ text: answer.text, pending: answer.pending });
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
/**
|
||||
* GET /api/conductor/fleet — fleet snapshot for the Conductor dashboard panel
|
||||
* (PRD Conductor RF3). Server-side proxy: the hub token lives only in env; the
|
||||
* response is the whitelisted shape from hubProxy (degraded {offline:true} when
|
||||
* the hub is unset/offline — never a 5xx for that).
|
||||
*/
|
||||
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
|
||||
import { getFleetSnapshot } from "@/lib/conductor/hubProxy";
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const authError = await requireManagementAuth(request);
|
||||
if (authError) return authError;
|
||||
return NextResponse.json(await getFleetSnapshot());
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
/**
|
||||
* POST /api/conductor/tasks/[id]/cancel — cancela a task no hub do Conductor.
|
||||
* Ação destrutiva: auth de gerência + confirmação na UI (ConfirmModal). A
|
||||
* recusa do hub volta só como status + mensagem sanitizada (nunca o corpo
|
||||
* upstream — Hard Rule #12).
|
||||
*/
|
||||
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
import { createErrorResponse } from "@/lib/api/errorResponse";
|
||||
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
|
||||
import { cancelConductorTask } from "@/lib/conductor/hubProxy";
|
||||
|
||||
export async function POST(request: Request, ctx: { params: Promise<{ id: string }> }) {
|
||||
const authError = await requireManagementAuth(request);
|
||||
if (authError) return authError;
|
||||
const { id } = await ctx.params;
|
||||
const result = await cancelConductorTask(id);
|
||||
if (!result.ok) {
|
||||
return createErrorResponse({
|
||||
status: result.status,
|
||||
message: `Conductor hub refused the cancellation (HTTP ${result.status})`,
|
||||
});
|
||||
}
|
||||
return NextResponse.json({ ok: true });
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
/**
|
||||
* GET /api/conductor/tasks/[id] — whitelisted task detail (manifest, prompt,
|
||||
* council funnel) from the Conductor hub. 404 sanitizado quando o hub não
|
||||
* conhece a task — o corpo do hub nunca é repassado.
|
||||
*/
|
||||
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
import { createErrorResponse } from "@/lib/api/errorResponse";
|
||||
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
|
||||
import { getConductorTaskDetail } from "@/lib/conductor/hubProxy";
|
||||
|
||||
export async function GET(request: Request, ctx: { params: Promise<{ id: string }> }) {
|
||||
const authError = await requireManagementAuth(request);
|
||||
if (authError) return authError;
|
||||
const { id } = await ctx.params;
|
||||
const detail = await getConductorTaskDetail(id);
|
||||
if (!detail) {
|
||||
return createErrorResponse({ status: 404, message: "Conductor task not found (or hub offline)" });
|
||||
}
|
||||
return NextResponse.json(detail);
|
||||
}
|
||||
@@ -1275,13 +1275,11 @@
|
||||
"groupSeparatorLabel": "Separator",
|
||||
"discovery": "Discovery",
|
||||
"discoverySubtitle": "Scan providers for free access",
|
||||
"conductor": "Conductor",
|
||||
"conductorSubtitle": "CLI-agent fleet",
|
||||
"resilienceConnections": "Connection Resilience",
|
||||
"resilienceConnectionsSubtitle": "Cooldown, breaker, lockout state",
|
||||
"settingsModalityBridge": "Modality Bridge",
|
||||
"settingsModalityBridgeSubtitle": "Image/audio → text fallback for text-only models",
|
||||
"commandPalette": {
|
||||
"resilienceConnections": "Connection Resilience",
|
||||
"resilienceConnectionsSubtitle": "Cooldown, breaker, lockout state",
|
||||
"settingsModalityBridge": "Modality Bridge",
|
||||
"settingsModalityBridgeSubtitle": "Image/audio → text fallback for text-only models",
|
||||
"commandPalette": {
|
||||
"title": "Command palette",
|
||||
"searchPlaceholder": "Search pages, settings, tools...",
|
||||
"clearSearch": "Clear search",
|
||||
@@ -6193,13 +6191,12 @@
|
||||
"cline": "Connect Cline with the existing OAuth flow.",
|
||||
"cursor": "Connect Cursor IDE with the existing OAuth flow.",
|
||||
"github": "Connect GitHub Copilot with the existing OAuth flow.",
|
||||
"gitlab-duo": "OAuth application with ai_features + read_user scopes. Configure GITLAB_DUO_OAUTH_CLIENT_ID and optionally GITLAB_DUO_OAUTH_CLIENT_SECRET on this OmniRoute instance.",
|
||||
"gitlab-duo": "GitLab Duo OAuth is not configured. Register an OAuth application at https://gitlab.com/-/profile/applications with redirect URI http://localhost:20128/callback and scopes \"ai_features read_user\", then set GITLAB_DUO_OAUTH_CLIENT_ID (and optionally GITLAB_DUO_OAUTH_CLIENT_SECRET) and restart.",
|
||||
"kilocode": "Connect Kilo Code with the existing OAuth flow.",
|
||||
"kimi-coding": "Connect Kimi Coding with the existing OAuth flow.",
|
||||
"kiro": "Free tier: 50 credits/month (~25K–100K tokens). ⚠️ Kiro ToS prohibits third-party proxy/harness use.",
|
||||
"codex": "Connect OpenAI Codex with the existing OAuth flow.",
|
||||
"qwen": "Connect Qwen Code with the existing OAuth flow.",
|
||||
"github-models": "Create a GitHub PAT with 'models: read' scope at github.com/settings/tokens"
|
||||
"qwen": "Connect Qwen Code with the existing OAuth flow."
|
||||
},
|
||||
"passthroughModelsDescription": "{provider} accepts provider-native model IDs. Import from /models or add custom IDs for routing.",
|
||||
"bedrockModelsDescription": "Amazon Bedrock models are scoped by AWS region. Import from /models or add Bedrock model IDs enabled in the selected region.",
|
||||
@@ -6249,87 +6246,86 @@
|
||||
"apiProtocolHint": "Some providers publish the same models over more than one protocol. Leave the default unless you need the alternative.",
|
||||
"bulkAddFormatHintCloudflare": "One key per line. Format: name|accountId|apiKey (Cloudflare account ID + API token).",
|
||||
"lmarenaWebCookieHint": "Open arena.ai, sign in, then copy the full Cookie header from a Network request. Include arena-auth-prod-v1.0 and arena-auth-prod-v1.1 (and further chunks if present), preferably with cf_clearance. Do not paste only the empty arena-auth-prod-v1 cookie. Optional: providerSpecificData.recaptchaV3Token if create-evaluation still returns 403.",
|
||||
"kimiOfficialSupporterBadge": "Official Supporter",
|
||||
"kimiOfficialSupporterTooltip": "Kimi (Moonshot AI) is an official OmniRoute launch partner",
|
||||
"kimiOfficialSupporterBadge": "Founding Friend",
|
||||
"kimiOfficialSupporterTooltip": "Kimi (Moonshot AI) is OmniRoute's founding Open Source Friend",
|
||||
"cheaperInferenceSupporterBadge": "Open Source Friend",
|
||||
"cheaperInferenceSupporterTooltip": "Cheaper Inference backs OmniRoute as an Open Source Friend",
|
||||
"kimiPartnerLinkNote": "Partner link — supports OmniRoute at no extra cost to you",
|
||||
"anonymousFallbackTitle": "Anonymous fallback",
|
||||
"anonymousFallbackDesc": "When all configured connections are exhausted (quota, credits, or expiry), temporarily use this provider's keyless tier. Turn off to skip this provider instead of sending anonymous requests — recommended when the keyless tier rejects them (401).",
|
||||
"anonymousFallbackEnabled": "Anonymous fallback enabled for {provider}",
|
||||
"anonymousFallbackDisabled": "Anonymous fallback disabled for {provider} — exhausted connections will skip this provider",
|
||||
"anonymousFallbackUpdateFailed": "Failed to update anonymous fallback setting",
|
||||
"batchDeleteFailed": "Batch delete failed",
|
||||
"batchDeleteNetworkError": "Network error during batch delete",
|
||||
"batchUpdateFailed": "Batch update failed",
|
||||
"batchUpdateNetworkError": "Network error during batch update",
|
||||
"categoryAudio": "Audio",
|
||||
"categoryCloudAgent": "Cloud Agent",
|
||||
"categoryIde": "IDE",
|
||||
"categoryLocal": "Local",
|
||||
"categorySearch": "Search",
|
||||
"categoryWebCookie": "Web Cookie",
|
||||
"claudeExtraUsageBlockingDisabled": "Claude extra-usage blocking disabled (extra usage is allowed)",
|
||||
"claudeExtraUsageBlockingEnabled": "Claude extra-usage blocking enabled (extra usage will be blocked)",
|
||||
"claudeRoutingPreferenceDisabled": "Unprefixed Claude models no longer prefer Claude Code",
|
||||
"claudeRoutingPreferenceEnabled": "Unprefixed Claude models now prefer Claude Code",
|
||||
"clearMediaFilter": "Clear",
|
||||
"cliproxyRoutingDisabled": "Requests now use native OmniRoute (direct)",
|
||||
"cliproxyRoutingEnabled": "Requests now route through CLIProxyAPI (deeper emulation)",
|
||||
"codexLimitPolicyUpdated": "Codex limit policy updated",
|
||||
"codexServiceModeUpdated": "Codex service mode updated",
|
||||
"codexServiceTierActive": "Codex {tier} service tier is active",
|
||||
"codexTierFastLabel": "Fast",
|
||||
"codexTierFlexLabel": "Flex",
|
||||
"commandCodeApplyFailed": "Failed to apply Command Code auth",
|
||||
"commandCodeApplyingApproval": "Browser approved, applying…",
|
||||
"commandCodeApplyingKey": "Applying browser-approved key…",
|
||||
"commandCodeApprovalInstructions": "Open the auth URL, approve access, then paste the returned key/JSON/URL below…",
|
||||
"commandCodeAuthExpired": "Command Code auth expired",
|
||||
"commandCodeConnected": "Command Code connected",
|
||||
"commandCodeConnectionAdded": "Command Code connection added",
|
||||
"commandCodeLinkExpired": "Command Code link expired",
|
||||
"commandCodeOpeningStudio": "Opening Command Code Studio…",
|
||||
"commandCodePopupBlocked": "Popup blocked. Please allow popups and try Command Code Connect again.",
|
||||
"commandCodeStartFailed": "Failed to start Command Code auth",
|
||||
"connectionDeleted": "Connection deleted",
|
||||
"connectionFallback": "connection",
|
||||
"coolingConnectionsDescription": "These connections returned a 429 (rate-limit) on their last request. OmniRoute will skip them until the timer expires — no manual disable required.",
|
||||
"coolingConnectionsTitle": "Currently cooling ({count})",
|
||||
"failedDeleteAlias": "Failed to delete alias",
|
||||
"failedDeleteConnection": "Failed to delete connection",
|
||||
"failedDistributeProxies": "Failed to distribute proxies.",
|
||||
"failedSaveModelEndpointSettings": "Failed to save model endpoint settings",
|
||||
"failedUpdateClaudeExtraUsagePolicy": "Failed to update Claude extra-usage policy",
|
||||
"failedUpdateClaudeRoutingPreference": "Failed to update Claude Code routing preference",
|
||||
"failedUpdateCliproxyRouting": "Failed to update CLIProxyAPI routing",
|
||||
"failedUpdateCodexLimitPolicy": "Failed to update Codex limit policy",
|
||||
"failedUpdateCodexServiceMode": "Failed to update Codex service mode",
|
||||
"filterByMedia": "Media",
|
||||
"freeBadge": "Free",
|
||||
"kimiCodeApiKeyLabel": "Kimi Code API Key",
|
||||
"modelTestFailed": "Model test failed",
|
||||
"modelTestNetworkError": "Network error testing model",
|
||||
"networkError": "Network error",
|
||||
"networkErrorDeletingAlias": "Network error deleting alias",
|
||||
"networkErrorSettingAlias": "Network error setting alias",
|
||||
"noProvidersMatch": "No providers match your search.",
|
||||
"noSavedProxies": "No saved proxies found. Add proxies in Settings → Proxy first.",
|
||||
"pageLoadErrorDescription": "We could not load provider data right now. Check your connection and try again.",
|
||||
"pageLoadErrorId": "Error ID: {id}",
|
||||
"pageLoadErrorRetry": "Try Again",
|
||||
"pageLoadErrorTitle": "Failed to load providers",
|
||||
"playgroundTitle": "Playground",
|
||||
"providerDetailConnectionFlexActive": "Codex flex service tier is active for this connection",
|
||||
"providerDetailConnectionPriorityActive": "Codex priority service tier is active for this connection",
|
||||
"providerDetailGlobalFlexActive": "Global Codex flex service tier is active",
|
||||
"providerDetailGlobalPriorityActive": "Global Codex priority service tier is active",
|
||||
"proxiesDistributed": "Distributed {assigned} proxy assignment(s) across {tagLabel}{total} connection(s).",
|
||||
"rerankEndpoint": "Rerank",
|
||||
"savedModelEndpointSettings": "Saved model endpoint settings",
|
||||
"searchByModelAria": "Search by model",
|
||||
"selectSupportedEndpoint": "Select at least one supported endpoint",
|
||||
"antigravityClientProfileHarness": "Harness / CLI"
|
||||
"anonymousFallbackTitle": "Anonymous fallback",
|
||||
"anonymousFallbackDesc": "When all configured connections are exhausted (quota, credits, or expiry), temporarily use this provider's keyless tier. Turn off to skip this provider instead of sending anonymous requests — recommended when the keyless tier rejects them (401).",
|
||||
"anonymousFallbackEnabled": "Anonymous fallback enabled for {provider}",
|
||||
"anonymousFallbackDisabled": "Anonymous fallback disabled for {provider} — exhausted connections will skip this provider",
|
||||
"anonymousFallbackUpdateFailed": "Failed to update anonymous fallback setting",
|
||||
"batchDeleteFailed": "Batch delete failed",
|
||||
"batchDeleteNetworkError": "Network error during batch delete",
|
||||
"batchUpdateFailed": "Batch update failed",
|
||||
"batchUpdateNetworkError": "Network error during batch update",
|
||||
"categoryAudio": "Audio",
|
||||
"categoryCloudAgent": "Cloud Agent",
|
||||
"categoryIde": "IDE",
|
||||
"categoryLocal": "Local",
|
||||
"categorySearch": "Search",
|
||||
"categoryWebCookie": "Web Cookie",
|
||||
"claudeExtraUsageBlockingDisabled": "Claude extra-usage blocking disabled (extra usage is allowed)",
|
||||
"claudeExtraUsageBlockingEnabled": "Claude extra-usage blocking enabled (extra usage will be blocked)",
|
||||
"claudeRoutingPreferenceDisabled": "Unprefixed Claude models no longer prefer Claude Code",
|
||||
"claudeRoutingPreferenceEnabled": "Unprefixed Claude models now prefer Claude Code",
|
||||
"clearMediaFilter": "Clear",
|
||||
"cliproxyRoutingDisabled": "Requests now use native OmniRoute (direct)",
|
||||
"cliproxyRoutingEnabled": "Requests now route through CLIProxyAPI (deeper emulation)",
|
||||
"codexLimitPolicyUpdated": "Codex limit policy updated",
|
||||
"codexServiceModeUpdated": "Codex service mode updated",
|
||||
"codexServiceTierActive": "Codex {tier} service tier is active",
|
||||
"codexTierFastLabel": "Fast",
|
||||
"codexTierFlexLabel": "Flex",
|
||||
"commandCodeApplyFailed": "Failed to apply Command Code auth",
|
||||
"commandCodeApplyingApproval": "Browser approved, applying…",
|
||||
"commandCodeApplyingKey": "Applying browser-approved key…",
|
||||
"commandCodeApprovalInstructions": "Open the auth URL, approve access, then paste the returned key/JSON/URL below…",
|
||||
"commandCodeAuthExpired": "Command Code auth expired",
|
||||
"commandCodeConnected": "Command Code connected",
|
||||
"commandCodeConnectionAdded": "Command Code connection added",
|
||||
"commandCodeLinkExpired": "Command Code link expired",
|
||||
"commandCodeOpeningStudio": "Opening Command Code Studio…",
|
||||
"commandCodePopupBlocked": "Popup blocked. Please allow popups and try Command Code Connect again.",
|
||||
"commandCodeStartFailed": "Failed to start Command Code auth",
|
||||
"connectionDeleted": "Connection deleted",
|
||||
"connectionFallback": "connection",
|
||||
"coolingConnectionsDescription": "These connections returned a 429 (rate-limit) on their last request. OmniRoute will skip them until the timer expires — no manual disable required.",
|
||||
"coolingConnectionsTitle": "Currently cooling ({count})",
|
||||
"failedDeleteAlias": "Failed to delete alias",
|
||||
"failedDeleteConnection": "Failed to delete connection",
|
||||
"failedDistributeProxies": "Failed to distribute proxies.",
|
||||
"failedSaveModelEndpointSettings": "Failed to save model endpoint settings",
|
||||
"failedUpdateClaudeExtraUsagePolicy": "Failed to update Claude extra-usage policy",
|
||||
"failedUpdateClaudeRoutingPreference": "Failed to update Claude Code routing preference",
|
||||
"failedUpdateCliproxyRouting": "Failed to update CLIProxyAPI routing",
|
||||
"failedUpdateCodexLimitPolicy": "Failed to update Codex limit policy",
|
||||
"failedUpdateCodexServiceMode": "Failed to update Codex service mode",
|
||||
"filterByMedia": "Media",
|
||||
"freeBadge": "Free",
|
||||
"kimiCodeApiKeyLabel": "Kimi Code API Key",
|
||||
"modelTestFailed": "Model test failed",
|
||||
"modelTestNetworkError": "Network error testing model",
|
||||
"networkError": "Network error",
|
||||
"networkErrorDeletingAlias": "Network error deleting alias",
|
||||
"networkErrorSettingAlias": "Network error setting alias",
|
||||
"noProvidersMatch": "No providers match your search.",
|
||||
"noSavedProxies": "No saved proxies found. Add proxies in Settings → Proxy first.",
|
||||
"pageLoadErrorDescription": "We could not load provider data right now. Check your connection and try again.",
|
||||
"pageLoadErrorId": "Error ID: {id}",
|
||||
"pageLoadErrorRetry": "Try Again",
|
||||
"pageLoadErrorTitle": "Failed to load providers",
|
||||
"playgroundTitle": "Playground",
|
||||
"providerDetailConnectionFlexActive": "Codex flex service tier is active for this connection",
|
||||
"providerDetailConnectionPriorityActive": "Codex priority service tier is active for this connection",
|
||||
"providerDetailGlobalFlexActive": "Global Codex flex service tier is active",
|
||||
"providerDetailGlobalPriorityActive": "Global Codex priority service tier is active",
|
||||
"proxiesDistributed": "Distributed {assigned} proxy assignment(s) across {tagLabel}{total} connection(s).",
|
||||
"rerankEndpoint": "Rerank",
|
||||
"savedModelEndpointSettings": "Saved model endpoint settings",
|
||||
"searchByModelAria": "Search by model",
|
||||
"selectSupportedEndpoint": "Select at least one supported endpoint"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Settings",
|
||||
@@ -6659,78 +6655,78 @@
|
||||
"NEEDS_CORE_NOT_CONFIGURED": "This subscription has nodes that need a local proxy core (SS/VMess/Trojan/VLESS); they are not routed until you configure the local-core SOCKS5 endpoint.",
|
||||
"NO_USABLE_NODES": "Subscription yielded no usable nodes (http/https/socks5 or nodes with a local core endpoint)."
|
||||
},
|
||||
"description": "Paste your proxy subscription link. Once enabled, traffic is routed through the proxy pool in global or rule (specified Provider) mode. Subscription nodes are automatically synced into the proxy pool and reuse existing polling, health-check, and anti-leak mechanisms.",
|
||||
"addSubscription": "Add Subscription",
|
||||
"newSubscription": "Add Subscription",
|
||||
"editSubscription": "Edit Subscription",
|
||||
"name": "Name",
|
||||
"namePlaceholder": "e.g. My Subscription A",
|
||||
"url": "Subscription URL",
|
||||
"urlPlaceholder": "https://.../subscribe?token=...",
|
||||
"mode": "Mode",
|
||||
"globalMode": "Global Mode",
|
||||
"ruleMode": "Rule Mode",
|
||||
"globalModeDesc": "All Provider traffic goes through this subscription's proxy pool.",
|
||||
"ruleModeDesc": "Only selected Providers' traffic goes through the proxy; the rest connect directly.",
|
||||
"localCoreEndpoint": "Local Core SOCKS5/HTTP Endpoint (Optional)",
|
||||
"localCoreEndpointPlaceholder": "socks5://127.0.0.1:1080",
|
||||
"localCoreEndpointDesc": "Only accepts 127.0.0.1 / localhost (SS/VMess/Trojan/VLESS require a local sing-box/clash core).",
|
||||
"routeByProvider": "Route by Provider (multi-select)",
|
||||
"loadingProviders": "Loading Provider list…",
|
||||
"autoRefreshInterval": "Auto Refresh Interval (minutes)",
|
||||
"enableAfterCreate": "Enable on creation (sync and take effect immediately)",
|
||||
"saving": "Saving…",
|
||||
"saveChanges": "Save Changes",
|
||||
"createSubscription": "Create Subscription",
|
||||
"loading": "Loading…",
|
||||
"noSubscriptions": "No subscriptions yet. Click \"Add Subscription\" to get started.",
|
||||
"statusOk": "OK",
|
||||
"statusError": "Error",
|
||||
"statusEmpty": "Empty",
|
||||
"global": "Global",
|
||||
"rule": "Rule",
|
||||
"enabled": "Enabled",
|
||||
"disabled": "Disabled",
|
||||
"nodeCount": "Nodes: {count}",
|
||||
"needsCoreCount": "{count} need local core",
|
||||
"lastSynced": "Last synced: {time}",
|
||||
"consecutiveFailures": "{count} consecutive failures",
|
||||
"lastError": "Last error: {time}",
|
||||
"coreHintTitle": "This subscription has {count} nodes that require a local proxy core (SS / VMess / Trojan / VLESS, etc.) and are currently not routed.",
|
||||
"coreHintDesc": "These protocols cannot be forwarded directly by OmniRoute. Please start a sing-box or clash (Clash.Meta) core on your machine, expose it as a SOCKS5/HTTP endpoint, and fill it in via Edit (only 127.0.0.1 / localhost is accepted).",
|
||||
"copy": "Copy",
|
||||
"goConfigure": "Configure",
|
||||
"disable": "Disable",
|
||||
"enable": "Enable",
|
||||
"refreshNodes": "Refresh Nodes",
|
||||
"edit": "Edit",
|
||||
"delete": "Delete",
|
||||
"confirmDelete": "Are you sure you want to delete subscription \"{name}\"? Associated proxy nodes will also be removed.",
|
||||
"nameRequired": "Please enter a name",
|
||||
"urlRequired": "Please enter a subscription URL",
|
||||
"ruleModeProviderRequired": "Please select at least one Provider in rule mode",
|
||||
"saveFailed": "Save failed",
|
||||
"loadFailed": "Failed to load subscription list",
|
||||
"toggleFailed": "Failed to toggle switch",
|
||||
"refreshFailed": "Failed to refresh",
|
||||
"deleteFailed": "Failed to delete",
|
||||
"add": "Add",
|
||||
"cancel": "Cancel",
|
||||
"configure": "Configure",
|
||||
"copyEndpoint": "Copy Endpoint",
|
||||
"coreEndpointHint": "Core Endpoint Hint",
|
||||
"coreNodesHint": "Core Nodes Hint",
|
||||
"create": "Create",
|
||||
"deleteConfirm": "Delete Confirm",
|
||||
"empty": "Empty",
|
||||
"globalModeDescription": "Global Mode Description",
|
||||
"localCoreHint": "Local Core Hint",
|
||||
"nodeSummary": "Node Summary",
|
||||
"providerRequired": "Provider Required",
|
||||
"providerRouting": "Provider Routing",
|
||||
"refresh": "Refresh",
|
||||
"refreshInterval": "Refresh Interval",
|
||||
"ruleModeDescription": "Rule Mode Description"
|
||||
"description": "Paste your proxy subscription link. Once enabled, traffic is routed through the proxy pool in global or rule (specified Provider) mode. Subscription nodes are automatically synced into the proxy pool and reuse existing polling, health-check, and anti-leak mechanisms.",
|
||||
"addSubscription": "Add Subscription",
|
||||
"newSubscription": "Add Subscription",
|
||||
"editSubscription": "Edit Subscription",
|
||||
"name": "Name",
|
||||
"namePlaceholder": "e.g. My Subscription A",
|
||||
"url": "Subscription URL",
|
||||
"urlPlaceholder": "https://.../subscribe?token=...",
|
||||
"mode": "Mode",
|
||||
"globalMode": "Global Mode",
|
||||
"ruleMode": "Rule Mode",
|
||||
"globalModeDesc": "All Provider traffic goes through this subscription's proxy pool.",
|
||||
"ruleModeDesc": "Only selected Providers' traffic goes through the proxy; the rest connect directly.",
|
||||
"localCoreEndpoint": "Local Core SOCKS5/HTTP Endpoint (Optional)",
|
||||
"localCoreEndpointPlaceholder": "socks5://127.0.0.1:1080",
|
||||
"localCoreEndpointDesc": "Only accepts 127.0.0.1 / localhost (SS/VMess/Trojan/VLESS require a local sing-box/clash core).",
|
||||
"routeByProvider": "Route by Provider (multi-select)",
|
||||
"loadingProviders": "Loading Provider list…",
|
||||
"autoRefreshInterval": "Auto Refresh Interval (minutes)",
|
||||
"enableAfterCreate": "Enable on creation (sync and take effect immediately)",
|
||||
"saving": "Saving…",
|
||||
"saveChanges": "Save Changes",
|
||||
"createSubscription": "Create Subscription",
|
||||
"loading": "Loading…",
|
||||
"noSubscriptions": "No subscriptions yet. Click \"Add Subscription\" to get started.",
|
||||
"statusOk": "OK",
|
||||
"statusError": "Error",
|
||||
"statusEmpty": "Empty",
|
||||
"global": "Global",
|
||||
"rule": "Rule",
|
||||
"enabled": "Enabled",
|
||||
"disabled": "Disabled",
|
||||
"nodeCount": "Nodes: {count}",
|
||||
"needsCoreCount": "{count} need local core",
|
||||
"lastSynced": "Last synced: {time}",
|
||||
"consecutiveFailures": "{count} consecutive failures",
|
||||
"lastError": "Last error: {time}",
|
||||
"coreHintTitle": "This subscription has {count} nodes that require a local proxy core (SS / VMess / Trojan / VLESS, etc.) and are currently not routed.",
|
||||
"coreHintDesc": "These protocols cannot be forwarded directly by OmniRoute. Please start a sing-box or clash (Clash.Meta) core on your machine, expose it as a SOCKS5/HTTP endpoint, and fill it in via Edit (only 127.0.0.1 / localhost is accepted).",
|
||||
"copy": "Copy",
|
||||
"goConfigure": "Configure",
|
||||
"disable": "Disable",
|
||||
"enable": "Enable",
|
||||
"refreshNodes": "Refresh Nodes",
|
||||
"edit": "Edit",
|
||||
"delete": "Delete",
|
||||
"confirmDelete": "Are you sure you want to delete subscription \"{name}\"? Associated proxy nodes will also be removed.",
|
||||
"nameRequired": "Please enter a name",
|
||||
"urlRequired": "Please enter a subscription URL",
|
||||
"ruleModeProviderRequired": "Please select at least one Provider in rule mode",
|
||||
"saveFailed": "Save failed",
|
||||
"loadFailed": "Failed to load subscription list",
|
||||
"toggleFailed": "Failed to toggle switch",
|
||||
"refreshFailed": "Failed to refresh",
|
||||
"deleteFailed": "Failed to delete",
|
||||
"add": "Add",
|
||||
"cancel": "Cancel",
|
||||
"configure": "Configure",
|
||||
"copyEndpoint": "Copy Endpoint",
|
||||
"coreEndpointHint": "Core Endpoint Hint",
|
||||
"coreNodesHint": "Core Nodes Hint",
|
||||
"create": "Create",
|
||||
"deleteConfirm": "Delete Confirm",
|
||||
"empty": "Empty",
|
||||
"globalModeDescription": "Global Mode Description",
|
||||
"localCoreHint": "Local Core Hint",
|
||||
"nodeSummary": "Node Summary",
|
||||
"providerRequired": "Provider Required",
|
||||
"providerRouting": "Provider Routing",
|
||||
"refresh": "Refresh",
|
||||
"refreshInterval": "Refresh Interval",
|
||||
"ruleModeDescription": "Rule Mode Description"
|
||||
},
|
||||
"bulkHealthcheck": "Bulk Healthcheck",
|
||||
"bulkHealthcheckDesc": "Test all configured proxies against a target URL to find which ones work.",
|
||||
@@ -6766,7 +6762,7 @@
|
||||
"denoRelayOrgDomainRequired": "Organization domain is required",
|
||||
"denoRelayDeployFailed": "Deno Deploy failed",
|
||||
"denoRelayTokenHint": "Organization token (prefix ddo_) from console.deno.com → Organization → Settings → Organization Tokens. Used once for deploy and never stored.",
|
||||
"denoRelayOrgDomainHint": "Your Deno Deploy organization's default domain (e.g. acme.deno.net). The relay will be reachable at https://<app-name>.<org-slug>.deno.net.",
|
||||
"denoRelayOrgDomainHint": "Your Deno Deploy organization's default domain (e.g. acme.deno.net). The relay will be reachable at https://<app-name>.<org-slug>.deno.net.",
|
||||
"proxyFreePoolFilterProtocol": "Filter by protocol",
|
||||
"proxyFreePoolProtocol": "Protocol",
|
||||
"proxyFreePoolCountryPlaceholder": "Country (e.g. US)",
|
||||
@@ -7905,9 +7901,9 @@
|
||||
"resilienceEnableServerWaitDesc": "When enabled, OmniRoute waits for the first cooldown to expire and retries automatically.",
|
||||
"resilienceMaxAttempts": "Maximum attempts",
|
||||
"resilienceMaxWaitPerAttempt": "Maximum wait per attempt",
|
||||
"resilienceComboCooldownWaitTitle": "Quota-share combo cooldown wait",
|
||||
"resilienceComboCooldownWaitDesc": "For quota-share combos only: wait out a short transient cooldown and re-dispatch instead of returning a 429 immediately. Never waits on quota_exhausted.",
|
||||
"resilienceComboCooldownWaitToggleDesc": "Quota-share combos only; never waits on quota_exhausted.",
|
||||
"resilienceComboCooldownWaitTitle": "Combo cooldown wait",
|
||||
"resilienceComboCooldownWaitDesc": "For all combo strategies: wait out a short transient cooldown and re-dispatch instead of returning a 429 immediately. Never waits on quota_exhausted.",
|
||||
"resilienceComboCooldownWaitToggleDesc": "All combo strategies; never waits on quota_exhausted.",
|
||||
"resilienceComboCooldownMaxWaitMs": "Maximum wait per attempt",
|
||||
"resilienceComboCooldownBudgetMs": "Total wait budget",
|
||||
"resilienceQuotaShareConcurrencyTitle": "Quota-share per-connection concurrency",
|
||||
@@ -8003,138 +7999,138 @@
|
||||
"enableCredentialRedactionDesc": "Scrubs API keys, tokens, private keys, and JWTs from messages, tool calls, and responses.",
|
||||
"pricingAutoSyncDisabled": "Automatic Sync Disabled",
|
||||
"pricingAutoSyncEnabled": "Automatic Sync Enabled",
|
||||
"modalityBridgeIntro": "Bridge multimodal content to text before it reaches text-only models. Vision is live; Audio arrives with the AudioBridge; Video is on the roadmap.",
|
||||
"modalityBridgeVisionTab": "Vision",
|
||||
"modalityBridgeAudioTab": "Audio",
|
||||
"modalityBridgeVideoTab": "Video",
|
||||
"modalityBridgeSubTabsAria": "Modality Bridge sections",
|
||||
"modalityBridgeVisionTitle": "Vision Bridge",
|
||||
"modalityBridgeVisionDesc": "Describe images with a vision model and continue with the user's chosen text model.",
|
||||
"modalityBridgeMode": "Mode",
|
||||
"modalityBridgeModeAuto": "Auto (recommended)",
|
||||
"modalityBridgeModeAutoHint": "Legacy heuristic: reroute individual models without credentials; describe otherwise.",
|
||||
"modalityBridgeModeDescribe": "Always describe",
|
||||
"modalityBridgeModeDescribeHint": "The model you chose always answers; images are replaced by text descriptions.",
|
||||
"modalityBridgeModeReroute": "Always reroute",
|
||||
"modalityBridgeModeRerouteHint": "Send the whole request to the best vision-capable model (falls back to describe when none is usable).",
|
||||
"modalityBridgeVisionModel": "Vision model",
|
||||
"modalityBridgeVisionModelAuto": "Auto (best available)",
|
||||
"modalityBridgeTaskAware": "Task-aware description",
|
||||
"modalityBridgeTaskAwareDesc": "Include the user's question as focus so the vision model describes what matters and transcribes visible text.",
|
||||
"modalityBridgePrompt": "Description prompt",
|
||||
"modalityBridgeAdvanced": "Advanced",
|
||||
"modalityBridgeTimeoutMs": "Timeout (ms)",
|
||||
"modalityBridgeMaxImages": "Max images per request",
|
||||
"modalityBridgeCacheEnabled": "Cache descriptions",
|
||||
"modalityBridgeCacheEnabledDesc": "Reuse descriptions for identical images (SHA-256 keyed, in-memory).",
|
||||
"modalityBridgeCacheTtlMinutes": "Cache TTL (minutes)",
|
||||
"modalityBridgeCacheMaxEntries": "Cache max entries",
|
||||
"modalityBridgeStatsBridged": "bridged",
|
||||
"modalityBridgeStatsCacheHits": "cache hits",
|
||||
"modalityBridgeStatsFailures": "failures",
|
||||
"modalityBridgeStatsLastUsed": "last used",
|
||||
"modalityBridgeStatsNever": "never",
|
||||
"modalityBridgeTestButton": "Test with sample image",
|
||||
"modalityBridgeTestRunning": "Testing…",
|
||||
"modalityBridgeTestOk": "Bridge OK — {count} image(s) described by {model}",
|
||||
"modalityBridgeTestReroute": "Bridge rerouted the request to {model}",
|
||||
"modalityBridgeTestNoop": "Bridge did not activate (model may support vision natively or bridge is disabled)",
|
||||
"modalityBridgeTestError": "Test failed: {message}",
|
||||
"modalityBridgeAudioComingSoon": "The Audio bridge (speech → text via /v1/audio/transcriptions) ships in the next release. Its settings keys are already reserved.",
|
||||
"modalityBridgeVideoComingSoon": "Video bridging (frame sampling + captioning) is on the backlog — see issue #9760.",
|
||||
"modalityBridgeMovedTitle": "Vision Bridge moved",
|
||||
"modalityBridgeMovedBody": "Vision Bridge settings now live in the dedicated Modality Bridge page.",
|
||||
"modalityBridgeMovedCta": "Open Modality Bridge settings",
|
||||
"modalityBridgeAudioTitle": "Audio Bridge",
|
||||
"modalityBridgeAudioDesc": "Transcribe audio with a speech-to-text model before continuing with the chosen text model.",
|
||||
"modalityBridgeAudioEnabled": "Enable Audio Bridge",
|
||||
"modalityBridgeAudioEnabledDesc": "Replace audio parts with transcripts when the target model cannot process audio.",
|
||||
"modalityBridgeAudioModel": "Speech-to-text model",
|
||||
"modalityBridgeAudioModelAuto": "Auto (first connected STT provider)",
|
||||
"modalityBridgeAudioMaxClips": "Max audio clips per request",
|
||||
"modalityBridgeAudioTestButton": "Test with sample audio",
|
||||
"modalityBridgeAudioTestRunning": "Testing audio…",
|
||||
"modalityBridgeAudioTestOk": "Audio Bridge OK — {count} clip(s) transcribed by {model}",
|
||||
"modalityBridgeAudioTestNoop": "Audio Bridge did not activate (the target may support audio, no STT provider is connected, or the bridge is disabled)",
|
||||
"modalityBridgeAudioTestError": "Audio test failed: {message}",
|
||||
"modelRoutingDescriptionPlaceholder": "Route Opus models to frontier combo",
|
||||
"cliproxyapiFallbackCodes": "Cliproxyapi Fallback Codes",
|
||||
"cliproxyapiFallbackDescription": "Cliproxyapi Fallback Description",
|
||||
"cliproxyapiHealthLabel": "Cliproxyapi Health Label",
|
||||
"cliproxyapiImportFailed": "Cliproxyapi Import Failed",
|
||||
"cliproxyapiImportResult": "Cliproxyapi Import Result",
|
||||
"cliproxyapiInvalidUrl": "Cliproxyapi Invalid URL",
|
||||
"cliproxyapiLifecycleNoticeAfter": "Cliproxyapi Lifecycle Notice After",
|
||||
"cliproxyapiLifecycleNoticeBefore": "Cliproxyapi Lifecycle Notice Before",
|
||||
"cliproxyapiLifecycleNoticeLink": "Cliproxyapi Lifecycle Notice Link",
|
||||
"cliproxyapiPortLabel": "Cliproxyapi Port Label",
|
||||
"cliproxyapiStatusLabel": "Cliproxyapi Status Label",
|
||||
"cliproxyapiVersionLabel": "Cliproxyapi Version Label",
|
||||
"collection": "Collection",
|
||||
"errorPage": {
|
||||
"modalityBridgeIntro": "Bridge multimodal content to text before it reaches text-only models. Vision is live; Audio arrives with the AudioBridge; Video is on the roadmap.",
|
||||
"modalityBridgeVisionTab": "Vision",
|
||||
"modalityBridgeAudioTab": "Audio",
|
||||
"modalityBridgeVideoTab": "Video",
|
||||
"modalityBridgeSubTabsAria": "Modality Bridge sections",
|
||||
"modalityBridgeVisionTitle": "Vision Bridge",
|
||||
"modalityBridgeVisionDesc": "Describe images with a vision model and continue with the user's chosen text model.",
|
||||
"modalityBridgeMode": "Mode",
|
||||
"modalityBridgeModeAuto": "Auto (recommended)",
|
||||
"modalityBridgeModeAutoHint": "Legacy heuristic: reroute individual models without credentials; describe otherwise.",
|
||||
"modalityBridgeModeDescribe": "Always describe",
|
||||
"modalityBridgeModeDescribeHint": "The model you chose always answers; images are replaced by text descriptions.",
|
||||
"modalityBridgeModeReroute": "Always reroute",
|
||||
"modalityBridgeModeRerouteHint": "Send the whole request to the best vision-capable model (falls back to describe when none is usable).",
|
||||
"modalityBridgeVisionModel": "Vision model",
|
||||
"modalityBridgeVisionModelAuto": "Auto (best available)",
|
||||
"modalityBridgeTaskAware": "Task-aware description",
|
||||
"modalityBridgeTaskAwareDesc": "Include the user's question as focus so the vision model describes what matters and transcribes visible text.",
|
||||
"modalityBridgePrompt": "Description prompt",
|
||||
"modalityBridgeAdvanced": "Advanced",
|
||||
"modalityBridgeTimeoutMs": "Timeout (ms)",
|
||||
"modalityBridgeMaxImages": "Max images per request",
|
||||
"modalityBridgeCacheEnabled": "Cache descriptions",
|
||||
"modalityBridgeCacheEnabledDesc": "Reuse descriptions for identical images (SHA-256 keyed, in-memory).",
|
||||
"modalityBridgeCacheTtlMinutes": "Cache TTL (minutes)",
|
||||
"modalityBridgeCacheMaxEntries": "Cache max entries",
|
||||
"modalityBridgeStatsBridged": "bridged",
|
||||
"modalityBridgeStatsCacheHits": "cache hits",
|
||||
"modalityBridgeStatsFailures": "failures",
|
||||
"modalityBridgeStatsLastUsed": "last used",
|
||||
"modalityBridgeStatsNever": "never",
|
||||
"modalityBridgeTestButton": "Test with sample image",
|
||||
"modalityBridgeTestRunning": "Testing…",
|
||||
"modalityBridgeTestOk": "Bridge OK — {count} image(s) described by {model}",
|
||||
"modalityBridgeTestReroute": "Bridge rerouted the request to {model}",
|
||||
"modalityBridgeTestNoop": "Bridge did not activate (model may support vision natively or bridge is disabled)",
|
||||
"modalityBridgeTestError": "Test failed: {message}",
|
||||
"modalityBridgeAudioComingSoon": "The Audio bridge (speech → text via /v1/audio/transcriptions) ships in the next release. Its settings keys are already reserved.",
|
||||
"modalityBridgeVideoComingSoon": "Video bridging (frame sampling + captioning) is on the backlog — see issue #9760.",
|
||||
"modalityBridgeMovedTitle": "Vision Bridge moved",
|
||||
"modalityBridgeMovedBody": "Vision Bridge settings now live in the dedicated Modality Bridge page.",
|
||||
"modalityBridgeMovedCta": "Open Modality Bridge settings",
|
||||
"modalityBridgeAudioTitle": "Audio Bridge",
|
||||
"modalityBridgeAudioDesc": "Transcribe audio with a speech-to-text model before continuing with the chosen text model.",
|
||||
"modalityBridgeAudioEnabled": "Enable Audio Bridge",
|
||||
"modalityBridgeAudioEnabledDesc": "Replace audio parts with transcripts when the target model cannot process audio.",
|
||||
"modalityBridgeAudioModel": "Speech-to-text model",
|
||||
"modalityBridgeAudioModelAuto": "Auto (first connected STT provider)",
|
||||
"modalityBridgeAudioMaxClips": "Max audio clips per request",
|
||||
"modalityBridgeAudioTestButton": "Test with sample audio",
|
||||
"modalityBridgeAudioTestRunning": "Testing audio…",
|
||||
"modalityBridgeAudioTestOk": "Audio Bridge OK — {count} clip(s) transcribed by {model}",
|
||||
"modalityBridgeAudioTestNoop": "Audio Bridge did not activate (the target may support audio, no STT provider is connected, or the bridge is disabled)",
|
||||
"modalityBridgeAudioTestError": "Audio test failed: {message}",
|
||||
"modelRoutingDescriptionPlaceholder": "Route Opus models to frontier combo",
|
||||
"cliproxyapiFallbackCodes": "Cliproxyapi Fallback Codes",
|
||||
"cliproxyapiFallbackDescription": "Cliproxyapi Fallback Description",
|
||||
"cliproxyapiHealthLabel": "Cliproxyapi Health Label",
|
||||
"cliproxyapiImportFailed": "Cliproxyapi Import Failed",
|
||||
"cliproxyapiImportResult": "Cliproxyapi Import Result",
|
||||
"cliproxyapiInvalidUrl": "Cliproxyapi Invalid URL",
|
||||
"cliproxyapiLifecycleNoticeAfter": "Cliproxyapi Lifecycle Notice After",
|
||||
"cliproxyapiLifecycleNoticeBefore": "Cliproxyapi Lifecycle Notice Before",
|
||||
"cliproxyapiLifecycleNoticeLink": "Cliproxyapi Lifecycle Notice Link",
|
||||
"cliproxyapiPortLabel": "Cliproxyapi Port Label",
|
||||
"cliproxyapiStatusLabel": "Cliproxyapi Status Label",
|
||||
"cliproxyapiVersionLabel": "Cliproxyapi Version Label",
|
||||
"collection": "Collection",
|
||||
"errorPage": {
|
||||
"description": "We could not load settings right now. Please retry in a few seconds.",
|
||||
"errorId": "Error ID: {id}",
|
||||
"retry": "Try Again",
|
||||
"title": "Failed to load settings"
|
||||
},
|
||||
"host": "Host",
|
||||
"notInstalled": "Not installed",
|
||||
"oneproxyActions": "Oneproxy Actions",
|
||||
"oneproxyActive": "Oneproxy Active",
|
||||
"oneproxyAnonymity": "Oneproxy Anonymity",
|
||||
"oneproxyCountry": "Oneproxy Country",
|
||||
"oneproxyDelete": "Oneproxy Delete",
|
||||
"oneproxyEmpty": "Oneproxy Empty",
|
||||
"oneproxyHost": "Oneproxy Host",
|
||||
"oneproxyLatency": "Oneproxy Latency",
|
||||
"oneproxyProtocol": "Oneproxy Protocol",
|
||||
"oneproxyQuality": "Oneproxy Quality",
|
||||
"oneproxySyncFailed": "Sync failed: {error}",
|
||||
"oneproxySyncSuccess": "Synced {total} proxies ({added} new, {updated} updated)",
|
||||
"proxyDocumentationSocks5DescAfter": "Proxy Documentation Socks5 Desc After",
|
||||
"proxyFreePoolAddProxy": "Proxy Free Pool Add Proxy",
|
||||
"proxyFreePoolAdding": "Proxy Free Pool Adding",
|
||||
"proxyFreePoolSelectProxy": "Proxy Free Pool Select Proxy",
|
||||
"proxyStatusActive": "Proxy Status Active",
|
||||
"proxyStatusInactive": "Proxy Status Inactive",
|
||||
"routingAddEntry": "Add entry",
|
||||
"settingSaveFailed": "Setting Save Failed",
|
||||
"settingSaved": "Setting Saved",
|
||||
"skillsmpApiKeyHintAfter": ". Rate limit: {limit} requests/day.",
|
||||
"skillsmpApiKeyHintBefore": "Get your API key from",
|
||||
"syncFailed": "Sync failed",
|
||||
"unhealthy": "Unhealthy",
|
||||
"oneproxyDescription": "Fetch and rotate free validated proxies from the 1proxy community platform",
|
||||
"oneproxySyncing": "Syncing...",
|
||||
"oneproxySyncNow": "Sync Now",
|
||||
"oneproxyClearAll": "Clear All",
|
||||
"oneproxyGoogle": "Google",
|
||||
"oneproxyClearAllConfirm": "Clear all 1proxy proxies?",
|
||||
"memorySkillsSkillsmpDescription": "Connect to SkillsMP to discover and install skills from the marketplace.",
|
||||
"memorySkillsActiveProviderDescription": "Choose which provider the Skills page uses for search and install.",
|
||||
"memorySkillsSkillsmpProviderTitle": "SkillsMP Marketplace",
|
||||
"memorySkillsSkillsmpProviderDescription": "Authenticated marketplace (uses your SkillsMP API key).",
|
||||
"memorySkillsSkillsshProviderTitle": "skills.sh Directory",
|
||||
"memorySkillsSkillsshProviderDescription": "Public directory provider (no API key required).",
|
||||
"routingCcBridgeCatalogName": "Anthropic-compatible CC bridge",
|
||||
"routingClaudeProviderName": "Claude (OAuth)",
|
||||
"routingClaudeProviderDescription": "Native Claude provider with OAuth-issued tokens.",
|
||||
"routingCcBridgeName": "Claude-Code Bridge",
|
||||
"routingCcBridgeDescription": "Relay endpoints using API keys (anthropic-compatible-cc-*).",
|
||||
"routingCustomProviderDescription": "Custom provider.",
|
||||
"routingUnknownOpKind": "Unknown op kind: {kind}",
|
||||
"routingInvalidJson": "Invalid JSON: {error}",
|
||||
"routingConfigMustBeObject": "Config must be a JSON object",
|
||||
"routingEnabledMustBeBoolean": "`enabled` must be true or false",
|
||||
"routingPipelineMustBeArray": "`pipeline` must be an array of ops",
|
||||
"routingPipelineTooLong": "Pipeline cannot exceed 50 ops",
|
||||
"routingOpMissingKind": "Op #{index}: missing or invalid `kind`",
|
||||
"routingOpUnknownKind": "Op #{index}: unknown kind \"{kind}\"",
|
||||
"routingJsonEditorHide": "Hide JSON editor",
|
||||
"routingJsonEditorImportExport": "Import / export JSON",
|
||||
"routingJsonEditorLabel": "JSON (edit and apply, or paste to import)",
|
||||
"routingApplyJson": "Apply JSON",
|
||||
"routingTransformsFootnote": "All transform ops are idempotent on re-run. Changes take effect immediately on the next request."
|
||||
"host": "Host",
|
||||
"notInstalled": "Not installed",
|
||||
"oneproxyActions": "Oneproxy Actions",
|
||||
"oneproxyActive": "Oneproxy Active",
|
||||
"oneproxyAnonymity": "Oneproxy Anonymity",
|
||||
"oneproxyCountry": "Oneproxy Country",
|
||||
"oneproxyDelete": "Oneproxy Delete",
|
||||
"oneproxyEmpty": "Oneproxy Empty",
|
||||
"oneproxyHost": "Oneproxy Host",
|
||||
"oneproxyLatency": "Oneproxy Latency",
|
||||
"oneproxyProtocol": "Oneproxy Protocol",
|
||||
"oneproxyQuality": "Oneproxy Quality",
|
||||
"oneproxySyncFailed": "Sync failed: {error}",
|
||||
"oneproxySyncSuccess": "Synced {total} proxies ({added} new, {updated} updated)",
|
||||
"proxyDocumentationSocks5DescAfter": "Proxy Documentation Socks5 Desc After",
|
||||
"proxyFreePoolAddProxy": "Proxy Free Pool Add Proxy",
|
||||
"proxyFreePoolAdding": "Proxy Free Pool Adding",
|
||||
"proxyFreePoolSelectProxy": "Proxy Free Pool Select Proxy",
|
||||
"proxyStatusActive": "Proxy Status Active",
|
||||
"proxyStatusInactive": "Proxy Status Inactive",
|
||||
"routingAddEntry": "Add entry",
|
||||
"settingSaveFailed": "Setting Save Failed",
|
||||
"settingSaved": "Setting Saved",
|
||||
"skillsmpApiKeyHintAfter": ". Rate limit: {limit} requests/day.",
|
||||
"skillsmpApiKeyHintBefore": "Get your API key from",
|
||||
"syncFailed": "Sync failed",
|
||||
"unhealthy": "Unhealthy",
|
||||
"oneproxyDescription": "Fetch and rotate free validated proxies from the 1proxy community platform",
|
||||
"oneproxySyncing": "Syncing...",
|
||||
"oneproxySyncNow": "Sync Now",
|
||||
"oneproxyClearAll": "Clear All",
|
||||
"oneproxyGoogle": "Google",
|
||||
"oneproxyClearAllConfirm": "Clear all 1proxy proxies?",
|
||||
"memorySkillsSkillsmpDescription": "Connect to SkillsMP to discover and install skills from the marketplace.",
|
||||
"memorySkillsActiveProviderDescription": "Choose which provider the Skills page uses for search and install.",
|
||||
"memorySkillsSkillsmpProviderTitle": "SkillsMP Marketplace",
|
||||
"memorySkillsSkillsmpProviderDescription": "Authenticated marketplace (uses your SkillsMP API key).",
|
||||
"memorySkillsSkillsshProviderTitle": "skills.sh Directory",
|
||||
"memorySkillsSkillsshProviderDescription": "Public directory provider (no API key required).",
|
||||
"routingCcBridgeCatalogName": "Anthropic-compatible CC bridge",
|
||||
"routingClaudeProviderName": "Claude (OAuth)",
|
||||
"routingClaudeProviderDescription": "Native Claude provider with OAuth-issued tokens.",
|
||||
"routingCcBridgeName": "Claude-Code Bridge",
|
||||
"routingCcBridgeDescription": "Relay endpoints using API keys (anthropic-compatible-cc-*).",
|
||||
"routingCustomProviderDescription": "Custom provider.",
|
||||
"routingUnknownOpKind": "Unknown op kind: {kind}",
|
||||
"routingInvalidJson": "Invalid JSON: {error}",
|
||||
"routingConfigMustBeObject": "Config must be a JSON object",
|
||||
"routingEnabledMustBeBoolean": "`enabled` must be true or false",
|
||||
"routingPipelineMustBeArray": "`pipeline` must be an array of ops",
|
||||
"routingPipelineTooLong": "Pipeline cannot exceed 50 ops",
|
||||
"routingOpMissingKind": "Op #{index}: missing or invalid `kind`",
|
||||
"routingOpUnknownKind": "Op #{index}: unknown kind \"{kind}\"",
|
||||
"routingJsonEditorHide": "Hide JSON editor",
|
||||
"routingJsonEditorImportExport": "Import / export JSON",
|
||||
"routingJsonEditorLabel": "JSON (edit and apply, or paste to import)",
|
||||
"routingApplyJson": "Apply JSON",
|
||||
"routingTransformsFootnote": "All transform ops are idempotent on re-run. Changes take effect immediately on the next request."
|
||||
},
|
||||
"contextRtk": {
|
||||
"title": "RTK Engine",
|
||||
@@ -10484,8 +10480,7 @@
|
||||
"gitlabDuoSetupTitle": "GitLab Duo OAuth setup",
|
||||
"gitlabDuoSetupMessage": "GitLab Duo OAuth is not configured. Register an OAuth application at {applicationsUrl} with redirect URI {redirectUri} and scopes \"{scopes}\", then set {clientIdEnv} (and optionally {clientSecretEnv}) and restart.",
|
||||
"gitlabDuoSetupDescription": "After the application is registered and the env vars are set on this OmniRoute instance, click Continue to start the OAuth login.",
|
||||
"continue": "Continue",
|
||||
"googleOAuthWarning": "Remote access + Google OAuth: bundled credentials only accept loopback redirects like <code>127.0.0.1</code>. The browser that approves Google must be able to reach OmniRoute on that local port, usually by opening OmniRoute locally or using an SSH/local-forward tunnel. Recommended for remote installs: on your own computer run <code>npx omniroute login antigravity</code> and paste the credential blob it prints into the field below. For fully remote use without this local callback, <a>configure your own OAuth credentials</a>."
|
||||
"continue": "Continue"
|
||||
},
|
||||
"cursorAuthModal": {
|
||||
"title": "Connect Cursor IDE",
|
||||
@@ -12323,13 +12318,13 @@
|
||||
"updateProviderFailed": "Failed to update provider",
|
||||
"providerEnabled": "{provider} enabled",
|
||||
"providerDisabled": "{provider} disabled",
|
||||
"providerAdded": "{provider} added",
|
||||
"add": "Add",
|
||||
"manualApiKey": "Use a manual API key",
|
||||
"createDahlTokenFailed": "Failed to create Dahl token",
|
||||
"providerProxy": "Proxy",
|
||||
"providerProxyConfigureHint": "Configure proxy",
|
||||
"providerProxyTitleConfigured": "Proxy configured: {host}"
|
||||
"providerAdded": "{provider} added",
|
||||
"add": "Add",
|
||||
"manualApiKey": "Use a manual API key",
|
||||
"createDahlTokenFailed": "Failed to create Dahl token",
|
||||
"providerProxy": "Proxy",
|
||||
"providerProxyConfigureHint": "Configure proxy",
|
||||
"providerProxyTitleConfigured": "Proxy configured: {host}"
|
||||
},
|
||||
"gamification": {
|
||||
"leaderboardScopes": {
|
||||
@@ -13023,14 +13018,14 @@
|
||||
"testFailed": "Test failed"
|
||||
},
|
||||
"kimiSponsorBanner": {
|
||||
"title": "Kimi (Moonshot AI) is now an official sponsor of OmniRoute",
|
||||
"title": "Kimi (Moonshot AI) is OmniRoute's founding Open Source Friend",
|
||||
"description": "Kimi K3 brings a 1M-token context window and frontier coding performance to OmniRoute at a fraction of the cost.",
|
||||
"cta": "Get Kimi Code",
|
||||
"partnerLinkNote": "Partner link",
|
||||
"dismissAriaLabel": "Dismiss"
|
||||
},
|
||||
"featureFlagExposeFunctionalGatewayMirrorsDescription": "Advertise <gateway-alias>/<model> mirror ids on /v1/models for models whose canonical owner has no active credential but a passthrough gateway with an active credential routes them. Warning: adds catalog entries for all clients when enabled globally.",
|
||||
"radarPage": {
|
||||
"radarPage": {
|
||||
"title": "Radar Catalog",
|
||||
"subtitle": "Free model catalog enriched with community intelligence",
|
||||
"loading": "Loading catalog...",
|
||||
@@ -13086,7 +13081,7 @@
|
||||
"campaignsUpsellCommunity": "Limited-time campaigns are a supporter extra. Everything on this page's fixed links stays free for everyone.",
|
||||
"campaignsValidUntil": "Valid until {date}"
|
||||
},
|
||||
"radarSetupPage": {
|
||||
"radarSetupPage": {
|
||||
"title": "Provider Setup",
|
||||
"setupTitle": "Setup: {provider}",
|
||||
"setupSubtitle": "Follow the steps below to configure this provider",
|
||||
@@ -13111,7 +13106,7 @@
|
||||
"addConnectionDescription": "Don't have a connection yet? Add one in the providers dashboard.",
|
||||
"addConnectionLink": "Go to providers →"
|
||||
},
|
||||
"resilienceConnections": {
|
||||
"resilienceConnections": {
|
||||
"title": "Connection Resilience",
|
||||
"table": {
|
||||
"status": "Status",
|
||||
@@ -13205,12 +13200,12 @@
|
||||
"degraded.source.modelLockouts": "Model Lockouts",
|
||||
"degraded.source.count": "Connection Count"
|
||||
},
|
||||
"featureFlagCapabilityFilterEnabledDescription": "Reject requests before dispatch when the target model lacks required capabilities (vision, tools, structured output, context window). Protects direct single-provider requests that bypass the combo-layer compatibility filter.",
|
||||
"capabilityFilter.visionMismatch": "Provider does not support vision for this image request",
|
||||
"capabilityFilter.toolsMismatch": "Provider does not support tool calling",
|
||||
"capabilityFilter.structuredOutputMismatch": "Provider does not support structured output",
|
||||
"capabilityFilter.contextWindowMismatch": "Request exceeds provider context window",
|
||||
"publicSystem": {
|
||||
"featureFlagCapabilityFilterEnabledDescription": "Reject requests before dispatch when the target model lacks required capabilities (vision, tools, structured output, context window). Protects direct single-provider requests that bypass the combo-layer compatibility filter.",
|
||||
"capabilityFilter.visionMismatch": "Provider does not support vision for this image request",
|
||||
"capabilityFilter.toolsMismatch": "Provider does not support tool calling",
|
||||
"capabilityFilter.structuredOutputMismatch": "Provider does not support structured output",
|
||||
"capabilityFilter.contextWindowMismatch": "Request exceeds provider context window",
|
||||
"publicSystem": {
|
||||
"notFound": {
|
||||
"title": "Page not found",
|
||||
"description": "The page you're looking for doesn't exist or has been moved.",
|
||||
@@ -13347,7 +13342,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"kiroAuthModal": {
|
||||
"kiroAuthModal": {
|
||||
"title": "Connect {providerLabel}",
|
||||
"chooseMethod": "Choose your authentication method:",
|
||||
"builderId": "AWS Builder ID",
|
||||
@@ -13389,7 +13384,7 @@
|
||||
"errorApiKeyImportFailed": "API key import failed",
|
||||
"errorIdcStartUrlRequired": "Please enter your IDC start URL"
|
||||
},
|
||||
"kiroSocialOAuthModal": {
|
||||
"kiroSocialOAuthModal": {
|
||||
"title": "Connect {providerLabel} via {providerName}",
|
||||
"errorStartAuthorization": "Failed to start authorization",
|
||||
"errorAuthorizationExpired": "Authorization expired. Start the login flow again.",
|
||||
@@ -13408,7 +13403,7 @@
|
||||
"errorTitle": "Connection Failed",
|
||||
"close": "Close"
|
||||
},
|
||||
"traeAuthModal": {
|
||||
"traeAuthModal": {
|
||||
"errorAuthorizationFailed": "Authorization failed",
|
||||
"errorPopupBlocked": "Popup blocked — allow popups for this site, or paste the token manually below.",
|
||||
"errorPopupClosed": "Authorization window was closed before completing.",
|
||||
@@ -13434,14 +13429,14 @@
|
||||
"importToken": "Import Token",
|
||||
"cancel": "Cancel"
|
||||
},
|
||||
"sharedComponents": {
|
||||
"sharedComponents": {
|
||||
"distributeProxies": {
|
||||
"distributing": "Distributing...",
|
||||
"complete": "Complete",
|
||||
"defaultLabel": "Distribute Proxies"
|
||||
}
|
||||
},
|
||||
"providerTest": {
|
||||
"providerTest": {
|
||||
"dialogLabel": "Test {provider}",
|
||||
"deprecated": "deprecated",
|
||||
"risk": "risk",
|
||||
@@ -13476,7 +13471,7 @@
|
||||
"tokens": "Tokens"
|
||||
}
|
||||
},
|
||||
"proxyLog": {
|
||||
"proxyLog": {
|
||||
"detailAriaLabel": "Proxy log detail",
|
||||
"event": "Proxy Event",
|
||||
"close": "Close proxy detail modal",
|
||||
@@ -13494,7 +13489,7 @@
|
||||
"error": "Error",
|
||||
"configuration": "Proxy Configuration"
|
||||
},
|
||||
"requestTimeline": {
|
||||
"requestTimeline": {
|
||||
"title": "Request Timeline",
|
||||
"modes": {
|
||||
"follow": "Follow",
|
||||
@@ -13519,62 +13514,12 @@
|
||||
"unknownModel": "unknown",
|
||||
"pending": "pending"
|
||||
},
|
||||
"metadata": {
|
||||
"metadata": {
|
||||
"compressionTitle": "Compression",
|
||||
"compressionDescription": "Configure context compression settings to reduce token usage and costs.",
|
||||
"relayTitle": "OmniRoute — Relay Proxies",
|
||||
"relayDescription": "Serverless relay proxy endpoints for your AI infrastructure",
|
||||
"trafficInspectorTitle": "Traffic Inspector — OmniRoute",
|
||||
"trafficInspectorDescription": "Monitor LLM calls + debug any application's HTTPS traffic"
|
||||
},
|
||||
"conductor": {
|
||||
"title": "Conductor — CLI-agent fleet",
|
||||
"subtitle": "OmniConductor hub: runners, task queue and councils, live",
|
||||
"runners": "Runners",
|
||||
"tasks": "Tasks",
|
||||
"colName": "Name",
|
||||
"colClis": "CLIs",
|
||||
"colStatus": "Status",
|
||||
"colTask": "Task",
|
||||
"colMode": "Mode",
|
||||
"colRunner": "Runner",
|
||||
"colSummary": "Summary",
|
||||
"online": "online",
|
||||
"offline": "offline",
|
||||
"draining": "draining",
|
||||
"hubOffline": "Conductor hub offline or not configured (CONDUCTOR_HUB_URL) — showing nothing rather than stale data.",
|
||||
"noRunners": "No runners registered",
|
||||
"noTasks": "No tasks yet",
|
||||
"detailTitle": "Task detail",
|
||||
"prompt": "Prompt",
|
||||
"branch": "Branch",
|
||||
"fetchHint": "Fetch the result with: git fetch origin {branch}",
|
||||
"council": "Council",
|
||||
"candidates": "Candidates",
|
||||
"cancel": "Cancel task",
|
||||
"cancelConfirmTitle": "Cancel this task?",
|
||||
"cancelConfirmMessage": "The hub will abort the execution on the runner. This cannot be undone.",
|
||||
"cancelFailed": "The hub refused the cancellation",
|
||||
"close": "Close",
|
||||
"error": "Error",
|
||||
"faroTitle": "Faro — fleet spokesperson",
|
||||
"faroSubtitle": "Chat and voice, anchored in real fleet events. Destructive commands always ask for confirmation.",
|
||||
"faroEmpty": "Ask about the fleet — e.g. \"how is the fleet?\"",
|
||||
"faroPending": "Faro is asking for confirmation",
|
||||
"faroPlaceholder": "talk to Faro…",
|
||||
"faroSend": "send",
|
||||
"faroOffline": "Faro (spokesperson) is offline",
|
||||
"yes": "Yes",
|
||||
"no": "No",
|
||||
"pushToTalk": "Hold to talk",
|
||||
"speakAnswers": "speak answers",
|
||||
"voiceModels": "Voice models (provider/model of this OmniRoute)",
|
||||
"voiceIdle": "talk",
|
||||
"voiceListening": "listening…",
|
||||
"voiceThinking": "thinking…",
|
||||
"voiceSpeaking": "speaking…",
|
||||
"sttFailed": "Transcription failed (check the STT model/provider)",
|
||||
"ttsFailed": "Speech synthesis failed (check the TTS model/provider)",
|
||||
"micDenied": "Microphone unavailable or permission denied"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,46 +0,0 @@
|
||||
/**
|
||||
* Server-side proxy to Faro, the OmniConductor spokesperson (Conductor PRD RF4).
|
||||
*
|
||||
* Faro's `/ask` requires a valid hub credential (Bearer) — that token lives only
|
||||
* in server env, so the browser talks to our /api/conductor/ask route, never to
|
||||
* Faro directly. The response is whitelisted to {text, pending}: `pending` set
|
||||
* means Faro is asking for confirmation (the UI offers Sim/Não); the safety gate
|
||||
* itself lives in Faro's engine and is never bypassed here.
|
||||
*/
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
const faroResponseSchema = z.object({
|
||||
text: z.string(),
|
||||
pending: z.unknown().nullish(),
|
||||
});
|
||||
|
||||
export interface FaroAnswer {
|
||||
ok: boolean;
|
||||
text: string;
|
||||
pending: unknown;
|
||||
}
|
||||
|
||||
export interface FaroProxyOptions {
|
||||
fetchImpl?: typeof fetch;
|
||||
}
|
||||
|
||||
const DEFAULT_FARO_URL = "http://127.0.0.1:7920";
|
||||
|
||||
export async function askFaro(message: string, opts: FaroProxyOptions = {}): Promise<FaroAnswer> {
|
||||
const base = process.env.CONDUCTOR_SPOKESPERSON_URL?.trim() || DEFAULT_FARO_URL;
|
||||
const token = process.env.CONDUCTOR_HUB_TOKEN?.trim() ?? "";
|
||||
try {
|
||||
const doFetch = opts.fetchImpl ?? fetch;
|
||||
const res = await doFetch(`${base}/ask`, {
|
||||
method: "POST",
|
||||
headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
|
||||
body: JSON.stringify({ message }),
|
||||
});
|
||||
if (!res.ok) return { ok: false, text: "", pending: null };
|
||||
const parsed = faroResponseSchema.parse(await res.json());
|
||||
return { ok: true, text: parsed.text, pending: parsed.pending ?? null };
|
||||
} catch {
|
||||
return { ok: false, text: "", pending: null };
|
||||
}
|
||||
}
|
||||
@@ -1,221 +0,0 @@
|
||||
/**
|
||||
* Server-side proxy to the OmniConductor hub (Conductor PRD RF3).
|
||||
*
|
||||
* The browser NEVER talks to the hub: these helpers run only in API routes,
|
||||
* authenticate with the server-side env token, and return WHITELISTED shapes —
|
||||
* runner/hub tokens can never leak to the client. Fail-open: hub unset/offline
|
||||
* yields a degraded snapshot ({offline: true}) instead of an error.
|
||||
*/
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
// ============ Whitelisted client-facing shapes ============
|
||||
|
||||
export interface FleetRunner {
|
||||
id: string;
|
||||
name: string;
|
||||
clis: string[];
|
||||
online: boolean;
|
||||
draining: boolean;
|
||||
}
|
||||
|
||||
export interface FleetTask {
|
||||
id: string;
|
||||
status: string;
|
||||
mode: string;
|
||||
repo: string | null;
|
||||
runner: string | null;
|
||||
summary: string | null;
|
||||
branch: string | null;
|
||||
error: string | null;
|
||||
updated_at: string | null;
|
||||
}
|
||||
|
||||
export interface FleetSnapshot {
|
||||
offline: boolean;
|
||||
runners: FleetRunner[];
|
||||
tasks: FleetTask[];
|
||||
}
|
||||
|
||||
export interface ConductorTaskDetail extends FleetTask {
|
||||
prompt: string | null;
|
||||
base_ref: string | null;
|
||||
tests: unknown;
|
||||
council: unknown;
|
||||
created_at: string | null;
|
||||
}
|
||||
|
||||
// ============ Untrusted hub shapes (parse only what we read) ============
|
||||
|
||||
const hubRunnerSchema = z.object({
|
||||
id: z.string(),
|
||||
online: z.boolean().optional(),
|
||||
draining: z.boolean().optional(),
|
||||
capabilities: z.object({
|
||||
name: z.string().optional(),
|
||||
clis: z.array(z.object({ profile: z.string() })).optional(),
|
||||
}),
|
||||
});
|
||||
|
||||
const hubTaskSchema = z.object({
|
||||
id: z.string(),
|
||||
status: z.string(),
|
||||
mode: z.string().optional(),
|
||||
repo: z.object({ url: z.string().optional(), base_ref: z.string().optional() }).nullish(),
|
||||
spec: z.object({ prompt: z.string().optional() }).nullish(),
|
||||
assigned_runner: z.string().nullish(),
|
||||
manifest: z
|
||||
.object({
|
||||
summary: z.string().nullish(),
|
||||
branch: z.string().nullish(),
|
||||
error: z.string().nullish(),
|
||||
tests: z.unknown().optional(),
|
||||
})
|
||||
.nullish(),
|
||||
council: z.unknown().optional(),
|
||||
created_at: z.string().optional(),
|
||||
updated_at: z.string().optional(),
|
||||
});
|
||||
|
||||
export interface HubProxyOptions {
|
||||
fetchImpl?: typeof fetch;
|
||||
}
|
||||
|
||||
function hubConfig(): { url: string; token: string } | null {
|
||||
const url = process.env.CONDUCTOR_HUB_URL?.trim();
|
||||
if (!url) return null;
|
||||
return { url, token: process.env.CONDUCTOR_HUB_TOKEN?.trim() ?? "" };
|
||||
}
|
||||
|
||||
async function hubGet(path: string, opts: HubProxyOptions): Promise<unknown | null> {
|
||||
const cfg = hubConfig();
|
||||
if (!cfg) return null;
|
||||
const doFetch = opts.fetchImpl ?? fetch;
|
||||
const res = await doFetch(`${cfg.url}${path}`, {
|
||||
headers: { authorization: `Bearer ${cfg.token}` },
|
||||
});
|
||||
if (!res.ok) return null;
|
||||
return res.json();
|
||||
}
|
||||
|
||||
function toFleetTask(t: z.infer<typeof hubTaskSchema>): FleetTask {
|
||||
return {
|
||||
id: t.id,
|
||||
status: t.status,
|
||||
mode: t.mode ?? "solo",
|
||||
repo: t.repo?.url ?? null,
|
||||
runner: t.assigned_runner ?? null,
|
||||
summary: t.manifest?.summary ?? null,
|
||||
branch: t.manifest?.branch ?? null,
|
||||
error: t.manifest?.error ?? null,
|
||||
updated_at: t.updated_at ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
/** Fleet snapshot for the dashboard panel. Degraded ({offline: true}) on any failure. */
|
||||
export async function getFleetSnapshot(opts: HubProxyOptions = {}): Promise<FleetSnapshot> {
|
||||
try {
|
||||
const [rawRunners, rawTasks] = await Promise.all([
|
||||
hubGet("/v1/runners", opts),
|
||||
hubGet("/v1/tasks", opts),
|
||||
]);
|
||||
if (rawRunners === null || rawTasks === null) return { offline: true, runners: [], tasks: [] };
|
||||
const runners = z.array(hubRunnerSchema).parse(rawRunners).map((r) => ({
|
||||
id: r.id,
|
||||
name: r.capabilities.name ?? "?",
|
||||
clis: (r.capabilities.clis ?? []).map((c) => c.profile),
|
||||
online: r.online !== false,
|
||||
draining: r.draining === true,
|
||||
}));
|
||||
const tasks = z.array(hubTaskSchema).parse(rawTasks).map(toFleetTask);
|
||||
return { offline: false, runners, tasks };
|
||||
} catch {
|
||||
return { offline: true, runners: [], tasks: [] };
|
||||
}
|
||||
}
|
||||
|
||||
/** Full whitelisted detail of one task (manifest, prompt, council funnel data). */
|
||||
export async function getConductorTaskDetail(
|
||||
taskId: string,
|
||||
opts: HubProxyOptions = {}
|
||||
): Promise<ConductorTaskDetail | null> {
|
||||
try {
|
||||
const raw = await hubGet(`/v1/tasks/${encodeURIComponent(taskId)}`, opts);
|
||||
if (raw === null) return null;
|
||||
const t = hubTaskSchema.parse(raw);
|
||||
return {
|
||||
...toFleetTask(t),
|
||||
prompt: t.spec?.prompt ?? null,
|
||||
base_ref: t.repo?.base_ref ?? null,
|
||||
tests: t.manifest?.tests ?? null,
|
||||
council: t.council ?? null,
|
||||
created_at: t.created_at ?? null,
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
export interface DelegationInput {
|
||||
repoUrl: string;
|
||||
prompt: string;
|
||||
baseRef?: string;
|
||||
mode?: string;
|
||||
cli?: string;
|
||||
model?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delegates work to the fleet: translates an external A2A task into the hub's
|
||||
* `POST /v1/tasks` (Conductor PRD RF5). Uses the orchestrator credential when
|
||||
* set (CONDUCTOR_ORCHESTRATOR_TOKEN), falling back to the hub token. States
|
||||
* flow back through the RF1 mirror — this call only creates.
|
||||
*/
|
||||
export async function createConductorTask(
|
||||
input: DelegationInput,
|
||||
opts: HubProxyOptions = {}
|
||||
): Promise<{ ok: boolean; status: number; task_id?: string }> {
|
||||
const cfg = hubConfig();
|
||||
if (!cfg) return { ok: false, status: 503 };
|
||||
const token = process.env.CONDUCTOR_ORCHESTRATOR_TOKEN?.trim() || cfg.token;
|
||||
const body: Record<string, unknown> = {
|
||||
repo: { url: input.repoUrl, base_ref: input.baseRef?.trim() || "main" },
|
||||
spec: { prompt: input.prompt },
|
||||
mode: input.mode?.trim() || "solo",
|
||||
};
|
||||
const requirements: Record<string, string> = {};
|
||||
if (input.cli?.trim()) requirements.cli = input.cli.trim();
|
||||
if (input.model?.trim()) requirements.model = input.model.trim();
|
||||
if (Object.keys(requirements).length) body.requirements = requirements;
|
||||
try {
|
||||
const doFetch = opts.fetchImpl ?? fetch;
|
||||
const res = await doFetch(`${cfg.url}/v1/tasks`, {
|
||||
method: "POST",
|
||||
headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
if (!res.ok) return { ok: false, status: res.status };
|
||||
const created = z.object({ id: z.string() }).parse(await res.json());
|
||||
return { ok: true, status: res.status, task_id: created.id };
|
||||
} catch {
|
||||
return { ok: false, status: 503 };
|
||||
}
|
||||
}
|
||||
|
||||
/** Cancels a task on the hub. Returns the hub's verdict without leaking its body on error. */
|
||||
export async function cancelConductorTask(
|
||||
taskId: string,
|
||||
opts: HubProxyOptions = {}
|
||||
): Promise<{ ok: boolean; status: number }> {
|
||||
const cfg = hubConfig();
|
||||
if (!cfg) return { ok: false, status: 503 };
|
||||
try {
|
||||
const doFetch = opts.fetchImpl ?? fetch;
|
||||
const res = await doFetch(`${cfg.url}/v1/tasks/${encodeURIComponent(taskId)}/cancel`, {
|
||||
method: "POST",
|
||||
headers: { authorization: `Bearer ${cfg.token}` },
|
||||
});
|
||||
return { ok: res.ok, status: res.status };
|
||||
} catch {
|
||||
return { ok: false, status: 503 };
|
||||
}
|
||||
}
|
||||
3
src/lib/env/runtimeEnv.ts
vendored
3
src/lib/env/runtimeEnv.ts
vendored
@@ -70,9 +70,6 @@ export const webRuntimeEnvSchema = z.object({
|
||||
BASE_URL: optionalHttpUrl,
|
||||
NEXT_PUBLIC_BASE_URL: optionalHttpUrl,
|
||||
CONDUCTOR_HUB_URL: optionalHttpUrl,
|
||||
CONDUCTOR_SPOKESPERSON_URL: optionalHttpUrl,
|
||||
CONDUCTOR_ORCHESTRATOR_TOKEN: optionalTrimmedString,
|
||||
|
||||
CONDUCTOR_HUB_TOKEN: optionalTrimmedString,
|
||||
OMNIROUTE_PORT: optionalPortEnv,
|
||||
API_PORT: optionalPortEnv,
|
||||
|
||||
@@ -7,33 +7,6 @@ export const PROVIDER_ENDPOINTS = {
|
||||
dgrid: "https://api.dgrid.ai/v1/chat/completions",
|
||||
bai: "https://api.b.ai/v1/chat/completions",
|
||||
qiniu: "https://api.qnaigc.com/v1/chat/completions",
|
||||
"zylo-api": "https://api.zyloai.net/v1/chat/completions",
|
||||
unorouter: "https://api.unorouter.com/v1/chat/completions",
|
||||
poolside: "https://inference.poolside.ai/v1/chat/completions",
|
||||
fastrouter: "https://api.fastrouter.ai/api/v1/chat/completions",
|
||||
anyapi: "https://api.anyapi.ai/v1/chat/completions",
|
||||
electronhub: "https://api.electronhub.ai/v1/chat/completions",
|
||||
llmgateway: "https://api.llmgateway.io/v1/chat/completions",
|
||||
"llm-kiwi": "https://api.llm.kiwi/v1/chat/completions",
|
||||
literouter: "https://api.literouter.com/v1/chat/completions",
|
||||
"mnn-ai": "https://api.mnnai.ru/v1/chat/completions",
|
||||
"meganova-ai": "https://api.meganova.ai/v1/chat/completions",
|
||||
mixlayer: "https://models.mixlayer.ai/v1/chat/completions",
|
||||
speka: "https://speka.me/v1/chat/completions",
|
||||
tokenreply: "https://api.tokenreply.com/v1/chat/completions",
|
||||
"yolo-auto": "https://yolo-auto.com/v1/chat/completions",
|
||||
dxnt: "https://www.dxnt.com/v1/chat/completions",
|
||||
"cloudcode-one": "https://api.cloudcode.one/v1/chat/completions",
|
||||
ofoxai: "https://api.ofox.ai/v1/chat/completions",
|
||||
zerolimitai: "https://www.zerolimitai.com/api/v1/chat/completions",
|
||||
chatanywhere: "https://api.chatanywhere.org/v1/chat/completions",
|
||||
helyxai: "https://helyxai.space/v1/chat/completions",
|
||||
auriko: "https://api.auriko.ai/v1/chat/completions",
|
||||
"poixe-ai": "https://api.poixe.com/v1/chat/completions",
|
||||
"naga-ai": "https://api.naga.ac/v1/chat/completions",
|
||||
"chat-oripe": "https://api.oriper.com/v1/chat/completions",
|
||||
freeinference: "https://freeinference.org/v1/chat/completions",
|
||||
"free-ai": "https://api.free.ai/v1/chat/",
|
||||
glm: "https://api.z.ai/api/anthropic/v1/messages",
|
||||
glmt: "https://api.z.ai/api/anthropic/v1/messages",
|
||||
"bailian-coding-plan": "https://coding-intl.dashscope.aliyuncs.com/apps/anthropic/v1/messages",
|
||||
|
||||
@@ -21,10 +21,7 @@ import { PROVIDER_MODELS as MODELS } from "@omniroute/open-sse/config/providerMo
|
||||
const PASSTHROUGH_PROVIDERS = new Set(
|
||||
Object.entries(AI_PROVIDERS)
|
||||
.filter(([, p]) => (p as any).passthroughModels)
|
||||
.flatMap(([key, provider]) => {
|
||||
const alias = (provider as { alias?: unknown }).alias;
|
||||
return typeof alias === "string" && alias.length > 0 ? [key, alias] : [key];
|
||||
})
|
||||
.map(([key]) => key)
|
||||
);
|
||||
|
||||
// Wrap isValidModel with passthrough providers
|
||||
|
||||
@@ -96,32 +96,7 @@ export const AGGREGATOR_PROVIDER_IDS = new Set([
|
||||
"g4f-nvidia",
|
||||
"naga-ac",
|
||||
"chatanywhere",
|
||||
"zylo-api",
|
||||
"fastrouter",
|
||||
"anyapi",
|
||||
"electronhub",
|
||||
"llmgateway",
|
||||
"llm-kiwi",
|
||||
"literouter",
|
||||
"mnn-ai",
|
||||
"meganova-ai",
|
||||
"mixlayer",
|
||||
"speka",
|
||||
"tokenreply",
|
||||
"yolo-auto",
|
||||
"dxnt",
|
||||
"cloudcode-one",
|
||||
"ofoxai",
|
||||
"zerolimitai",
|
||||
"helyxai",
|
||||
"auriko",
|
||||
"poixe-ai",
|
||||
"naga-ai",
|
||||
"chat-oripe",
|
||||
"freeinference",
|
||||
"free-ai",
|
||||
|
||||
]);;
|
||||
]);
|
||||
|
||||
export const ENTERPRISE_CLOUD_PROVIDER_IDS = new Set([
|
||||
"azure-openai",
|
||||
|
||||
@@ -53,9 +53,6 @@ export const APIKEY_PROVIDERS_GATEWAYS = {
|
||||
color: "#8B5CF6",
|
||||
textIcon: "UR",
|
||||
passthroughModels: true,
|
||||
hasFree: true,
|
||||
freeNote:
|
||||
"Models with the :free suffix do not debit balance; limit is 1 request/minute per free model per user.",
|
||||
website: "https://unorouter.ai",
|
||||
apiHint: "Create an API key at https://unorouter.ai, then paste it here as a Bearer token.",
|
||||
},
|
||||
@@ -442,37 +439,6 @@ export const APIKEY_PROVIDERS_GATEWAYS = {
|
||||
apiHint:
|
||||
"Use https://api.oriper.com/v1 only after confirming the provider's current documentation, terms and key issuance. No quota is guaranteed by this catalog.",
|
||||
},
|
||||
freeinference: {
|
||||
id: "freeinference",
|
||||
alias: "freeinference",
|
||||
name: "FreeInference",
|
||||
icon: "science",
|
||||
color: "#8B5CF6",
|
||||
textIcon: "FI",
|
||||
passthroughModels: true,
|
||||
website: "https://freeinference.org",
|
||||
hasFree: true,
|
||||
freeNote:
|
||||
"Free research access without a card; non-Harvard applicants require manual approval and no numeric quota is publicly guaranteed.",
|
||||
apiHint:
|
||||
"Apply for a FreeInference key, then use https://freeinference.org/v1 as the OpenAI-compatible base URL. Terms allow prompt/response logging and possible publication of anonymized research data; never send sensitive or production data.",
|
||||
},
|
||||
"free-ai": {
|
||||
id: "free-ai",
|
||||
alias: "free-ai",
|
||||
name: "Free.ai",
|
||||
icon: "hub",
|
||||
color: "#16A34A",
|
||||
textIcon: "FA",
|
||||
passthroughModels: true,
|
||||
website: "https://free.ai",
|
||||
hasFree: true,
|
||||
freeNote:
|
||||
"30,000 tokens/day cover self-hosted models after email verification. Usage beyond the pool can bill at raw cost, and premium external models are paid.",
|
||||
apiHint:
|
||||
"Create an sk-free- key, then use the nonstandard but OpenAI-shaped https://api.free.ai/v1/chat/ endpoint. Select a self-hosted zero-price model to stay within the free pool.",
|
||||
},
|
||||
|
||||
dgrid: {
|
||||
id: "dgrid",
|
||||
alias: "dgrid",
|
||||
@@ -1213,7 +1179,6 @@ export const APIKEY_PROVIDERS_GATEWAYS = {
|
||||
authHint: "Get your Regolo API key from regolo.ai, then paste it here as a Bearer token.",
|
||||
apiHint:
|
||||
"OpenAI-compatible endpoint at https://api.regolo.ai/v1 with dynamic model discovery (19 models).",
|
||||
},
|
||||
"naga-ac": {
|
||||
id: "naga-ac",
|
||||
alias: "naga",
|
||||
@@ -1230,4 +1195,19 @@ export const APIKEY_PROVIDERS_GATEWAYS = {
|
||||
authHint:
|
||||
"Get API key at naga.ac — Google/GitHub/Discord signup available.",
|
||||
},
|
||||
chatanywhere: {
|
||||
id: "chatanywhere",
|
||||
alias: "chtany",
|
||||
name: "ChatAnywhere",
|
||||
icon: "chat",
|
||||
color: "#10B981",
|
||||
textIcon: "CA",
|
||||
website: "https://api.chatanywhere.tech",
|
||||
hasFree: true,
|
||||
freeNote:
|
||||
"Free tier: 5 req/day for GPT-5/4o/4.1, 30/day DeepSeek, 200/day gpt-4o-mini. Personal non-commercial use only — see chatanywhere/GPT_API_free. Requires GitHub-account-gated API key.",
|
||||
passthroughModels: true,
|
||||
authHint:
|
||||
"Get free API key at api.chatanywhere.tech — requires GitHub account signup.",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -45,21 +45,6 @@ export const APIKEY_PROVIDERS_INFERENCE = {
|
||||
hasFree: true,
|
||||
freeNote: "Free plan: 3-day trial with open-source models — no credit card required",
|
||||
},
|
||||
poolside: {
|
||||
id: "poolside",
|
||||
alias: "poolside",
|
||||
name: "Poolside",
|
||||
icon: "memory",
|
||||
color: "#111827",
|
||||
textIcon: "PS",
|
||||
passthroughModels: true,
|
||||
website: "https://poolside.ai",
|
||||
hasFree: true,
|
||||
freeNote:
|
||||
"Laguna S 2.1 and XS 2.1 are free during Preview; no public numeric quota is published.",
|
||||
apiHint:
|
||||
"Create a free developer API key, then use https://inference.poolside.ai/v1 as the OpenAI-compatible base URL.",
|
||||
},
|
||||
fireworks: {
|
||||
id: "fireworks",
|
||||
alias: "fireworks",
|
||||
|
||||
@@ -243,15 +243,6 @@ const TOOLS_GROUP: SidebarItemGroup = {
|
||||
subtitleKey: "cloudAgentsSubtitle",
|
||||
icon: "cloud",
|
||||
},
|
||||
{
|
||||
id: "conductor",
|
||||
href: "/dashboard/conductor",
|
||||
i18nKey: "conductor",
|
||||
subtitleKey: "conductorSubtitle",
|
||||
icon: "account_tree",
|
||||
labelFallback: "Conductor",
|
||||
subtitleFallback: "CLI-agent fleet",
|
||||
},
|
||||
{
|
||||
id: "agent-bridge",
|
||||
href: "/dashboard/tools/agent-bridge",
|
||||
|
||||
@@ -29,7 +29,6 @@ export const HIDEABLE_SIDEBAR_ITEM_IDS = [
|
||||
"cli-agents",
|
||||
"acp-agents",
|
||||
"cloud-agents",
|
||||
"conductor",
|
||||
"agent-bridge",
|
||||
"traffic-inspector",
|
||||
"discovery",
|
||||
|
||||
@@ -6,7 +6,9 @@
|
||||
import { test, after } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
const { registerHook, unregisterHook } = await import("../../src/lib/plugins/hooks.ts");const { runPluginOnResponseHook, runPluginOnStreamCompleteHook } = await import("../../open-sse/handlers/chatCore/pluginOnResponse.ts");
|
||||
const { registerHook, unregisterHook } = await import("../../src/lib/plugins/hooks.ts");
|
||||
const { runPluginOnResponseHook } =
|
||||
await import("../../open-sse/handlers/chatCore/pluginOnResponse.ts");
|
||||
|
||||
async function waitFor(pred: () => boolean, timeoutMs = 2000): Promise<void> {
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
@@ -17,7 +19,6 @@ async function waitFor(pred: () => boolean, timeoutMs = 2000): Promise<void> {
|
||||
|
||||
after(() => {
|
||||
unregisterHook("onResponse", "test-onresponse-plugin");
|
||||
unregisterHook("onStreamComplete", "test-onstreamcomplete-plugin");
|
||||
});
|
||||
|
||||
test("no registered hooks → resolves without throwing (no-op)", async () => {
|
||||
@@ -142,140 +143,3 @@ test("a throwing hook never rejects the caller (fail-open)", async () => {
|
||||
);
|
||||
await new Promise((r) => setTimeout(r, 30));
|
||||
});
|
||||
|
||||
// ── onStreamComplete hook tests (#9571) ──
|
||||
|
||||
test("onStreamComplete: no registered hooks resolves without throwing (no-op)", async () => {
|
||||
const start = Date.now();
|
||||
await assert.doesNotReject(
|
||||
runPluginOnStreamCompleteHook({
|
||||
status: 200,
|
||||
usage: { prompt_tokens: 10, completion_tokens: 20 },
|
||||
ttft: 150,
|
||||
model: "gpt-4",
|
||||
provider: "openai",
|
||||
errorCode: undefined,
|
||||
startTime: start - 500,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
test("onStreamComplete: registered hook receives usage + timing payload", async () => {
|
||||
let captured: Record<string, unknown> | undefined;
|
||||
registerHook(
|
||||
"onStreamComplete",
|
||||
"test-onstreamcomplete-plugin",
|
||||
async (payload: Record<string, unknown>) => {
|
||||
captured = payload;
|
||||
}
|
||||
);
|
||||
|
||||
const startTime = Date.now() - 500;
|
||||
await runPluginOnStreamCompleteHook({
|
||||
status: 200,
|
||||
usage: { prompt_tokens: 42, completion_tokens: 100, reasoning_tokens: 5 },
|
||||
ttft: 200,
|
||||
model: "claude-3-opus",
|
||||
provider: "anthropic",
|
||||
errorCode: undefined,
|
||||
startTime,
|
||||
});
|
||||
|
||||
await waitFor(() => captured !== undefined);
|
||||
assert.ok(captured, "expected onStreamComplete hook to be invoked");
|
||||
|
||||
// payload shape: status, usage, timing, model, provider
|
||||
assert.equal(captured!.status, 200);
|
||||
assert.ok(captured!.usage, "usage should be present");
|
||||
assert.equal((captured!.usage as Record<string, number>).prompt_tokens, 42);
|
||||
assert.equal((captured!.usage as Record<string, number>).completion_tokens, 100);
|
||||
assert.equal((captured!.usage as Record<string, number>).reasoning_tokens, 5);
|
||||
|
||||
assert.ok(captured!.timing, "timing should be present");
|
||||
const timing = captured!.timing as Record<string, number>;
|
||||
assert.equal(timing.ttft, 200);
|
||||
assert.ok(timing.latencyMs > 450, "latencyMs should be near 500");
|
||||
|
||||
assert.equal(captured!.model, "claude-3-opus");
|
||||
assert.equal(captured!.provider, "anthropic");
|
||||
assert.equal(captured!.errorCode, undefined);
|
||||
});
|
||||
|
||||
test("onStreamComplete: payload includes cache token fields when present", async () => {
|
||||
let captured: Record<string, unknown> | undefined;
|
||||
registerHook(
|
||||
"onStreamComplete",
|
||||
"test-onstreamcomplete-plugin",
|
||||
async (payload: Record<string, unknown>) => {
|
||||
captured = payload;
|
||||
}
|
||||
);
|
||||
|
||||
await runPluginOnStreamCompleteHook({
|
||||
status: 200,
|
||||
usage: {
|
||||
prompt_tokens: 50,
|
||||
completion_tokens: 30,
|
||||
cache_read_input_tokens: 20,
|
||||
cache_creation_input_tokens: 10,
|
||||
},
|
||||
ttft: 100,
|
||||
model: "gpt-4",
|
||||
provider: "openai",
|
||||
errorCode: undefined,
|
||||
startTime: Date.now(),
|
||||
});
|
||||
|
||||
await waitFor(() => captured !== undefined);
|
||||
assert.ok(captured);
|
||||
const usage = captured!.usage as Record<string, number>;
|
||||
assert.equal(usage.cache_read_input_tokens, 20);
|
||||
assert.equal(usage.cache_creation_input_tokens, 10);
|
||||
});
|
||||
|
||||
test("onStreamComplete: throwing hook never rejects the caller (fail-open)", async () => {
|
||||
registerHook("onStreamComplete", "test-onstreamcomplete-plugin", async () => {
|
||||
throw new Error("stream-complete-boom");
|
||||
});
|
||||
|
||||
await assert.doesNotReject(
|
||||
runPluginOnStreamCompleteHook({
|
||||
status: 500,
|
||||
usage: undefined,
|
||||
ttft: undefined,
|
||||
model: "gpt-4",
|
||||
provider: "openai",
|
||||
errorCode: "upstream_error",
|
||||
startTime: Date.now(),
|
||||
})
|
||||
);
|
||||
await new Promise((r) => setTimeout(r, 30));
|
||||
});
|
||||
|
||||
test("onStreamComplete: errorCode is passed through when provided", async () => {
|
||||
let captured: Record<string, unknown> | undefined;
|
||||
registerHook(
|
||||
"onStreamComplete",
|
||||
"test-onstreamcomplete-plugin",
|
||||
async (payload: Record<string, unknown>) => {
|
||||
captured = payload;
|
||||
}
|
||||
);
|
||||
|
||||
await runPluginOnStreamCompleteHook({
|
||||
status: 502,
|
||||
usage: undefined,
|
||||
ttft: undefined,
|
||||
model: "grok-3",
|
||||
provider: "xai",
|
||||
errorCode: "upstream_timeout",
|
||||
startTime: Date.now(),
|
||||
});
|
||||
|
||||
await waitFor(() => captured !== undefined);
|
||||
assert.ok(captured);
|
||||
assert.equal(captured!.status, 502);
|
||||
assert.equal(captured!.errorCode, "upstream_timeout");
|
||||
assert.equal(captured!.model, "grok-3");
|
||||
assert.equal(captured!.provider, "xai");
|
||||
});
|
||||
|
||||
@@ -1,112 +0,0 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { createServer, type Server } from "node:http";
|
||||
|
||||
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-conductor-a2a-"));
|
||||
process.env.DATA_DIR = TEST_DATA_DIR;
|
||||
|
||||
const core = await import("../../src/lib/db/core.ts");
|
||||
const settings = await import("../../src/lib/db/settings.ts");
|
||||
const tasksRoute = await import("../../src/app/api/a2a/tasks/route.ts");
|
||||
|
||||
const servers: Server[] = [];
|
||||
|
||||
async function enableA2A() {
|
||||
await settings.updateSettings({ a2aEnabled: true });
|
||||
}
|
||||
|
||||
function delegationRequest(body: unknown, bearer?: string) {
|
||||
return new Request("http://localhost/api/a2a/tasks", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
...(bearer ? { authorization: `Bearer ${bearer}` } : {}),
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
}
|
||||
|
||||
const VALID_BODY = {
|
||||
skill: "conductor-cli-claude",
|
||||
messages: [{ role: "user", content: "adicione um README com a seção Sobre" }],
|
||||
metadata: { conductor: { repo: { url: "https://git.x/repo", base_ref: "dev" }, mode: "solo", model: "cc/claude-sonnet-5" } },
|
||||
};
|
||||
|
||||
test.beforeEach(() => {
|
||||
core.resetDbInstance();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
|
||||
delete process.env.CONDUCTOR_HUB_URL;
|
||||
delete process.env.OMNIROUTE_API_KEY;
|
||||
});
|
||||
|
||||
test.after(async () => {
|
||||
core.resetDbInstance();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
delete process.env.CONDUCTOR_HUB_URL;
|
||||
delete process.env.OMNIROUTE_API_KEY;
|
||||
while (servers.length > 0) {
|
||||
const s = servers.pop();
|
||||
await new Promise((resolve) => s?.close(resolve));
|
||||
}
|
||||
});
|
||||
|
||||
test("A2A desabilitado → 503 (mesmo gate do JSON-RPC)", async () => {
|
||||
const res = await tasksRoute.POST(delegationRequest(VALID_BODY));
|
||||
assert.equal(res.status, 503);
|
||||
});
|
||||
|
||||
test("com OMNIROUTE_API_KEY configurada, bearer errado → 401 e bearer certo passa", async () => {
|
||||
await enableA2A();
|
||||
process.env.OMNIROUTE_API_KEY = "chave-certa";
|
||||
const denied = await tasksRoute.POST(delegationRequest(VALID_BODY, "chave-errada"));
|
||||
assert.equal(denied.status, 401);
|
||||
});
|
||||
|
||||
test("delegação válida → 201 com o task_id do hub; requirements derivados da skill do card", async () => {
|
||||
await enableA2A();
|
||||
const bodies: unknown[] = [];
|
||||
await new Promise<void>((resolve) => {
|
||||
const server = createServer((req, res) => {
|
||||
let raw = "";
|
||||
req.on("data", (c) => (raw += c));
|
||||
req.on("end", () => {
|
||||
bodies.push(JSON.parse(raw));
|
||||
res.writeHead(201, { "content-type": "application/json" });
|
||||
res.end(JSON.stringify({ id: "t_delegada", status: "submitted" }));
|
||||
});
|
||||
});
|
||||
servers.push(server);
|
||||
server.listen(0, "127.0.0.1", () => {
|
||||
const addr = server.address();
|
||||
process.env.CONDUCTOR_HUB_URL = `http://127.0.0.1:${typeof addr === "object" && addr ? addr.port : 0}`;
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
const res = await tasksRoute.POST(delegationRequest(VALID_BODY));
|
||||
assert.equal(res.status, 201);
|
||||
const out = await res.json();
|
||||
assert.equal(out.conductor_task_id, "t_delegada");
|
||||
assert.equal(out.state, "submitted");
|
||||
const sent = bodies[0] as { repo: { url: string; base_ref: string }; spec: { prompt: string }; requirements: { cli: string; model: string } };
|
||||
assert.equal(sent.repo.url, "https://git.x/repo");
|
||||
assert.equal(sent.repo.base_ref, "dev");
|
||||
assert.equal(sent.spec.prompt, "adicione um README com a seção Sobre");
|
||||
assert.equal(sent.requirements.cli, "claude", "skill conductor-cli-claude vira requirements.cli");
|
||||
assert.equal(sent.requirements.model, "cc/claude-sonnet-5");
|
||||
});
|
||||
|
||||
test("sem repo na metadata → 400 (delegação exige repo); skill não-conductor → 400", async () => {
|
||||
await enableA2A();
|
||||
const noRepo = await tasksRoute.POST(
|
||||
delegationRequest({ skill: "conductor", messages: [{ role: "user", content: "p" }] })
|
||||
);
|
||||
assert.equal(noRepo.status, 400);
|
||||
const wrongSkill = await tasksRoute.POST(
|
||||
delegationRequest({ ...VALID_BODY, skill: "smart-routing" })
|
||||
);
|
||||
assert.equal(wrongSkill.status, 400);
|
||||
});
|
||||
@@ -1,85 +0,0 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { createServer, type Server } from "node:http";
|
||||
|
||||
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-conductor-ask-"));
|
||||
process.env.DATA_DIR = TEST_DATA_DIR;
|
||||
|
||||
const core = await import("../../src/lib/db/core.ts");
|
||||
const askRoute = await import("../../src/app/api/conductor/ask/route.ts");
|
||||
|
||||
const servers: Server[] = [];
|
||||
|
||||
test.beforeEach(() => {
|
||||
core.resetDbInstance();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
|
||||
delete process.env.CONDUCTOR_SPOKESPERSON_URL;
|
||||
});
|
||||
|
||||
test.after(async () => {
|
||||
core.resetDbInstance();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
delete process.env.CONDUCTOR_SPOKESPERSON_URL;
|
||||
while (servers.length > 0) {
|
||||
const s = servers.pop();
|
||||
await new Promise((resolve) => s?.close(resolve));
|
||||
}
|
||||
});
|
||||
|
||||
test("fonte: auth antes do proxy; token nunca manuseado na rota", () => {
|
||||
const src = fs.readFileSync(path.join(process.cwd(), "src/app/api/conductor/ask/route.ts"), "utf8");
|
||||
const authAt = src.indexOf("requireManagementAuth(");
|
||||
assert.ok(authAt > 0);
|
||||
assert.match(src, /if \(authError\) return authError;/);
|
||||
assert.ok(src.indexOf("askFaro(") > authAt, "askFaro só depois do gate");
|
||||
assert.ok(!src.includes("CONDUCTOR_HUB_TOKEN"), "token vive no faroProxy, não na rota");
|
||||
});
|
||||
|
||||
test("POST valida o body (Zod) e repassa text+pending do Faro", async () => {
|
||||
await new Promise<void>((resolve) => {
|
||||
const server = createServer((req, res) => {
|
||||
res.writeHead(200, { "content-type": "application/json" });
|
||||
res.end(JSON.stringify({ text: "frota vazia", pending: null }));
|
||||
});
|
||||
servers.push(server);
|
||||
server.listen(0, "127.0.0.1", () => {
|
||||
const addr = server.address();
|
||||
process.env.CONDUCTOR_SPOKESPERSON_URL = `http://127.0.0.1:${typeof addr === "object" && addr ? addr.port : 0}`;
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
const ok = await askRoute.POST(
|
||||
new Request("http://localhost/api/conductor/ask", {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({ message: "como está a frota?" }),
|
||||
})
|
||||
);
|
||||
assert.equal(ok.status, 200);
|
||||
assert.deepEqual(await ok.json(), { text: "frota vazia", pending: null });
|
||||
|
||||
const bad = await askRoute.POST(
|
||||
new Request("http://localhost/api/conductor/ask", {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({ message: "" }),
|
||||
})
|
||||
);
|
||||
assert.equal(bad.status, 400, "mensagem vazia é rejeitada pelo Zod");
|
||||
});
|
||||
|
||||
test("Faro fora do ar → 503 sanitizado", async () => {
|
||||
process.env.CONDUCTOR_SPOKESPERSON_URL = "http://127.0.0.1:1";
|
||||
const res = await askRoute.POST(
|
||||
new Request("http://localhost/api/conductor/ask", {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({ message: "oi" }),
|
||||
})
|
||||
);
|
||||
assert.equal(res.status, 503);
|
||||
});
|
||||
@@ -1,71 +0,0 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import { createConductorTask } from "../../src/lib/conductor/hubProxy.ts";
|
||||
|
||||
function fakeHub(body: unknown, status = 201) {
|
||||
const calls: { url: string; method: string; auth: string | null; body: unknown }[] = [];
|
||||
const impl = (async (url: string | URL | Request, init?: RequestInit) => {
|
||||
calls.push({
|
||||
url: String(url),
|
||||
method: init?.method ?? "GET",
|
||||
auth: (init?.headers as Record<string, string> | undefined)?.authorization ?? null,
|
||||
body: JSON.parse(String(init?.body ?? "{}")),
|
||||
});
|
||||
return new Response(JSON.stringify(body), { status });
|
||||
}) as typeof fetch;
|
||||
return { impl, calls };
|
||||
}
|
||||
|
||||
test.beforeEach(() => {
|
||||
process.env.CONDUCTOR_HUB_URL = "http://hub.test:7910";
|
||||
process.env.CONDUCTOR_HUB_TOKEN = "tok-hub";
|
||||
delete process.env.CONDUCTOR_ORCHESTRATOR_TOKEN;
|
||||
});
|
||||
|
||||
test.after(() => {
|
||||
delete process.env.CONDUCTOR_HUB_URL;
|
||||
delete process.env.CONDUCTOR_HUB_TOKEN;
|
||||
delete process.env.CONDUCTOR_ORCHESTRATOR_TOKEN;
|
||||
});
|
||||
|
||||
test("traduz a delegação A2A no POST /v1/tasks do hub (shape exato do contrato)", async () => {
|
||||
const { impl, calls } = fakeHub({ id: "t_novo", status: "submitted" });
|
||||
const r = await createConductorTask(
|
||||
{ repoUrl: "https://git.x/repo", baseRef: "dev", prompt: "adicione um README", mode: "council-3", cli: "claude", model: "cc/claude-sonnet-5" },
|
||||
{ fetchImpl: impl }
|
||||
);
|
||||
assert.deepEqual(r, { ok: true, status: 201, task_id: "t_novo" });
|
||||
assert.equal(calls[0].url, "http://hub.test:7910/v1/tasks");
|
||||
assert.equal(calls[0].method, "POST");
|
||||
assert.deepEqual(calls[0].body, {
|
||||
repo: { url: "https://git.x/repo", base_ref: "dev" },
|
||||
spec: { prompt: "adicione um README" },
|
||||
mode: "council-3",
|
||||
requirements: { cli: "claude", model: "cc/claude-sonnet-5" },
|
||||
});
|
||||
});
|
||||
|
||||
test("defaults: base_ref main, mode solo, sem requirements quando cli/model ausentes", async () => {
|
||||
const { impl, calls } = fakeHub({ id: "t_d", status: "submitted" });
|
||||
await createConductorTask({ repoUrl: "https://git.x/r", prompt: "p" }, { fetchImpl: impl });
|
||||
assert.deepEqual(calls[0].body, { repo: { url: "https://git.x/r", base_ref: "main" }, spec: { prompt: "p" }, mode: "solo" });
|
||||
});
|
||||
|
||||
test("credencial: prefere CONDUCTOR_ORCHESTRATOR_TOKEN; fallback é o token do hub", async () => {
|
||||
const a = fakeHub({ id: "t_1" });
|
||||
await createConductorTask({ repoUrl: "https://x/r", prompt: "p" }, { fetchImpl: a.impl });
|
||||
assert.equal(a.calls[0].auth, "Bearer tok-hub");
|
||||
process.env.CONDUCTOR_ORCHESTRATOR_TOKEN = "tok-orch";
|
||||
const b = fakeHub({ id: "t_2" });
|
||||
await createConductorTask({ repoUrl: "https://x/r", prompt: "p" }, { fetchImpl: b.impl });
|
||||
assert.equal(b.calls[0].auth, "Bearer tok-orch");
|
||||
});
|
||||
|
||||
test("recusa do hub → {ok:false, status} sem lançar nem vazar corpo; env ausente → 503", async () => {
|
||||
const { impl } = fakeHub({ error: "segredo do hub" }, 422);
|
||||
const r = await createConductorTask({ repoUrl: "https://x/r", prompt: "p" }, { fetchImpl: impl });
|
||||
assert.deepEqual(r, { ok: false, status: 422 });
|
||||
delete process.env.CONDUCTOR_HUB_URL;
|
||||
assert.deepEqual(await createConductorTask({ repoUrl: "https://x/r", prompt: "p" }, {}), { ok: false, status: 503 });
|
||||
});
|
||||
@@ -1,39 +0,0 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
// Invariantes de segurança/UX do chat com voz (componentes React: vitest-ui advisory + dashboard-typecheck).
|
||||
const CHAT = "src/app/(dashboard)/dashboard/conductor/FaroChat.tsx";
|
||||
|
||||
function src(): string {
|
||||
return fs.readFileSync(path.join(process.cwd(), CHAT), "utf8");
|
||||
}
|
||||
|
||||
test("client fala SÓ com o OmniRoute: /api/conductor/ask + /api/v1/audio/* (nunca Faro/hub direto)", () => {
|
||||
const s = src();
|
||||
assert.match(s, /"use client"/);
|
||||
assert.match(s, /\/api\/conductor\/ask/);
|
||||
assert.match(s, /\/api\/v1\/audio\/transcriptions/);
|
||||
assert.match(s, /\/api\/v1\/audio\/speech/);
|
||||
assert.ok(!s.includes(":7920"), "endereço do Faro nunca no client");
|
||||
assert.ok(!s.includes("CONDUCTOR_"), "nenhuma env do Conductor no client");
|
||||
});
|
||||
|
||||
test("pending do Faro → botões Sim/Não que enviam 'sim'/'não' (trava de confirmação é do motor do Faro)", () => {
|
||||
const s = src();
|
||||
assert.match(s, /pending/);
|
||||
assert.match(s, /"sim"/);
|
||||
assert.match(s, /"não"/);
|
||||
});
|
||||
|
||||
test("voz: push-to-talk com MediaRecorder/getUserMedia; STT multipart sem Content-Type manual; TTS via Blob com revoke", () => {
|
||||
const s = src();
|
||||
assert.match(s, /navigator\.mediaDevices\.getUserMedia/);
|
||||
assert.match(s, /MediaRecorder/);
|
||||
assert.match(s, /FormData\(\)/);
|
||||
assert.ok(!/audio\/transcriptions[\s\S]{0,300}content-type/i.test(s), "multipart deixa o browser definir o boundary");
|
||||
assert.match(s, /URL\.createObjectURL/);
|
||||
assert.match(s, /URL\.revokeObjectURL/);
|
||||
assert.match(s, /useTranslations\("conductor"\)/);
|
||||
});
|
||||
@@ -1,61 +0,0 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import { askFaro } from "../../src/lib/conductor/faroProxy.ts";
|
||||
|
||||
function fakeFaro(body: unknown, status = 200) {
|
||||
const calls: { url: string; auth: string | null; body: unknown }[] = [];
|
||||
const impl = (async (url: string | URL | Request, init?: RequestInit) => {
|
||||
calls.push({
|
||||
url: String(url),
|
||||
auth: (init?.headers as Record<string, string> | undefined)?.authorization ?? null,
|
||||
body: JSON.parse(String(init?.body ?? "{}")),
|
||||
});
|
||||
return new Response(JSON.stringify(body), { status });
|
||||
}) as typeof fetch;
|
||||
return { impl, calls };
|
||||
}
|
||||
|
||||
test.beforeEach(() => {
|
||||
process.env.CONDUCTOR_SPOKESPERSON_URL = "http://faro.test:7920";
|
||||
process.env.CONDUCTOR_HUB_TOKEN = "tok-hub";
|
||||
});
|
||||
|
||||
test.after(() => {
|
||||
delete process.env.CONDUCTOR_SPOKESPERSON_URL;
|
||||
delete process.env.CONDUCTOR_HUB_TOKEN;
|
||||
});
|
||||
|
||||
test("repassa a mensagem ao /ask com o token server-side e devolve text+pending", async () => {
|
||||
const { impl, calls } = fakeFaro({ text: "frota ok", pending: { kind: "cancel_task" }, extra: "NÃO passa" });
|
||||
const r = await askFaro("como está a frota?", { fetchImpl: impl });
|
||||
assert.deepEqual(r, { ok: true, text: "frota ok", pending: { kind: "cancel_task" } });
|
||||
assert.equal(calls[0].url, "http://faro.test:7920/ask");
|
||||
assert.equal(calls[0].auth, "Bearer tok-hub");
|
||||
assert.deepEqual(calls[0].body, { message: "como está a frota?" });
|
||||
});
|
||||
|
||||
test("pending null passa como null (sem confirmação pendente)", async () => {
|
||||
const { impl } = fakeFaro({ text: "oi", pending: null });
|
||||
const r = await askFaro("oi", { fetchImpl: impl });
|
||||
assert.deepEqual(r, { ok: true, text: "oi", pending: null });
|
||||
});
|
||||
|
||||
test("Faro fora do ar / erro HTTP → degradado {ok:false} sem lançar nem vazar corpo", async () => {
|
||||
const failing = (async () => {
|
||||
throw new Error("ECONNREFUSED");
|
||||
}) as unknown as typeof fetch;
|
||||
const down = await askFaro("oi", { fetchImpl: failing });
|
||||
assert.equal(down.ok, false);
|
||||
const { impl } = fakeFaro({ error: "segredo interno" }, 401);
|
||||
const denied = await askFaro("oi", { fetchImpl: impl });
|
||||
assert.equal(denied.ok, false);
|
||||
assert.ok(!JSON.stringify(denied).includes("segredo interno"));
|
||||
});
|
||||
|
||||
test("URL default do Faro é loopback :7920 quando a env não está setada", async () => {
|
||||
delete process.env.CONDUCTOR_SPOKESPERSON_URL;
|
||||
const { impl, calls } = fakeFaro({ text: "x", pending: null });
|
||||
await askFaro("oi", { fetchImpl: impl });
|
||||
assert.equal(calls[0].url, "http://127.0.0.1:7920/ask");
|
||||
});
|
||||
@@ -1,103 +0,0 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { createServer, type Server } from "node:http";
|
||||
|
||||
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-conductor-route-"));
|
||||
process.env.DATA_DIR = TEST_DATA_DIR;
|
||||
|
||||
const core = await import("../../src/lib/db/core.ts");
|
||||
const fleetRoute = await import("../../src/app/api/conductor/fleet/route.ts");
|
||||
const detailRoute = await import("../../src/app/api/conductor/tasks/[id]/route.ts");
|
||||
const cancelRoute = await import("../../src/app/api/conductor/tasks/[id]/cancel/route.ts");
|
||||
|
||||
const servers: Server[] = [];
|
||||
|
||||
function fakeHub(routes: Record<string, { status: number; body: unknown }>): Promise<string> {
|
||||
const server = createServer((req, res) => {
|
||||
const hit = Object.entries(routes).find(([p]) => (req.url ?? "").startsWith(p));
|
||||
res.writeHead(hit ? hit[1].status : 404, { "content-type": "application/json" });
|
||||
res.end(JSON.stringify(hit ? hit[1].body : { error: "hub: segredo interno que NÃO pode vazar" }));
|
||||
});
|
||||
servers.push(server);
|
||||
return new Promise((resolve) => {
|
||||
server.listen(0, "127.0.0.1", () => {
|
||||
const addr = server.address();
|
||||
resolve(`http://127.0.0.1:${typeof addr === "object" && addr ? addr.port : 0}`);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
test.beforeEach(() => {
|
||||
core.resetDbInstance();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
|
||||
delete process.env.CONDUCTOR_HUB_URL;
|
||||
delete process.env.CONDUCTOR_HUB_TOKEN;
|
||||
});
|
||||
|
||||
test.after(async () => {
|
||||
core.resetDbInstance();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
delete process.env.CONDUCTOR_HUB_URL;
|
||||
while (servers.length > 0) {
|
||||
const s = servers.pop();
|
||||
await new Promise((resolve) => s?.close(resolve));
|
||||
}
|
||||
});
|
||||
|
||||
test("GET /api/conductor/fleet devolve snapshot whitelisted; sem hub → degradado 200", async () => {
|
||||
process.env.CONDUCTOR_HUB_URL = await fakeHub({
|
||||
"/v1/runners": {
|
||||
status: 200,
|
||||
body: [{ id: "r_1", token: "VAZOU?", online: true, capabilities: { name: "devbox", clis: [{ profile: "claude" }] } }],
|
||||
},
|
||||
"/v1/tasks": {
|
||||
status: 200,
|
||||
body: [{ id: "t_1", status: "working", mode: "solo", repo: { url: "https://x/r" }, assigned_runner: "r_1" }],
|
||||
},
|
||||
});
|
||||
process.env.CONDUCTOR_HUB_TOKEN = "tok";
|
||||
const res = await fleetRoute.GET(new Request("http://localhost/api/conductor/fleet"));
|
||||
assert.equal(res.status, 200);
|
||||
const body = await res.json();
|
||||
assert.equal(body.offline, false);
|
||||
assert.equal(body.runners[0].name, "devbox");
|
||||
assert.equal(body.tasks[0].status, "working");
|
||||
assert.ok(!JSON.stringify(body).includes("VAZOU?"), "token de runner não vaza pela rota");
|
||||
|
||||
delete process.env.CONDUCTOR_HUB_URL; // sem hub: degradado, nunca 500
|
||||
const down = await fleetRoute.GET(new Request("http://localhost/api/conductor/fleet"));
|
||||
assert.equal(down.status, 200);
|
||||
assert.equal((await down.json()).offline, true);
|
||||
});
|
||||
|
||||
test("GET /api/conductor/tasks/[id] → 404 sanitizado quando o hub não conhece a task", async () => {
|
||||
process.env.CONDUCTOR_HUB_URL = await fakeHub({});
|
||||
const res = await detailRoute.GET(new Request("http://localhost/api/conductor/tasks/t_x"), {
|
||||
params: Promise.resolve({ id: "t_x" }),
|
||||
});
|
||||
assert.equal(res.status, 404);
|
||||
const text = await res.text();
|
||||
assert.ok(!text.includes("segredo interno"), "corpo do hub NUNCA repassado");
|
||||
});
|
||||
|
||||
test("POST cancel repassa recusa do hub com status, sem corpo upstream", async () => {
|
||||
process.env.CONDUCTOR_HUB_URL = await fakeHub({
|
||||
"/v1/tasks/t_done/cancel": { status: 409, body: { error: "segredo interno que NÃO pode vazar" } },
|
||||
"/v1/tasks/t_ok/cancel": { status: 200, body: { ok: true } },
|
||||
});
|
||||
const denied = await cancelRoute.POST(new Request("http://localhost/x", { method: "POST" }), {
|
||||
params: Promise.resolve({ id: "t_done" }),
|
||||
});
|
||||
assert.equal(denied.status, 409);
|
||||
assert.ok(!(await denied.text()).includes("segredo interno"));
|
||||
|
||||
const ok = await cancelRoute.POST(new Request("http://localhost/x", { method: "POST" }), {
|
||||
params: Promise.resolve({ id: "t_ok" }),
|
||||
});
|
||||
assert.equal(ok.status, 200);
|
||||
assert.deepEqual(await ok.json(), { ok: true });
|
||||
});
|
||||
@@ -1,140 +0,0 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import {
|
||||
getFleetSnapshot,
|
||||
getConductorTaskDetail,
|
||||
cancelConductorTask,
|
||||
} from "../../src/lib/conductor/hubProxy.ts";
|
||||
|
||||
const RUNNERS = [
|
||||
{
|
||||
id: "r_1",
|
||||
token: "NUNCA-VAZAR",
|
||||
online: true,
|
||||
draining: false,
|
||||
capabilities: { name: "devbox", clis: [{ profile: "claude" }, { profile: "codex" }], skills: [] },
|
||||
},
|
||||
];
|
||||
|
||||
const TASKS = [
|
||||
{
|
||||
id: "t_1",
|
||||
status: "completed",
|
||||
mode: "solo",
|
||||
from: "orchestrator",
|
||||
repo: { url: "https://git.x/repo", base_ref: "main" },
|
||||
spec: { prompt: "faz algo" },
|
||||
assigned_runner: "r_1",
|
||||
manifest: { summary: "feito", branch: "task/t_1", error: null },
|
||||
council: null,
|
||||
created_at: "2026-07-22T00:00:00Z",
|
||||
updated_at: "2026-07-22T00:01:00Z",
|
||||
},
|
||||
{
|
||||
id: "t_2",
|
||||
status: "working",
|
||||
mode: "council-3",
|
||||
from: "orchestrator",
|
||||
repo: { url: "https://git.x/repo", base_ref: "main" },
|
||||
spec: { prompt: "outra" },
|
||||
assigned_runner: null,
|
||||
manifest: null,
|
||||
council: { candidate_task_ids: ["t_2a", "t_2b"] },
|
||||
created_at: "2026-07-22T00:02:00Z",
|
||||
updated_at: "2026-07-22T00:02:30Z",
|
||||
},
|
||||
];
|
||||
|
||||
function fakeHub(routes: Record<string, { status: number; body: unknown }>) {
|
||||
const calls: { url: string; method: string; auth: string | null }[] = [];
|
||||
const impl = (async (url: string | URL | Request, init?: RequestInit) => {
|
||||
const u = String(url);
|
||||
calls.push({
|
||||
url: u,
|
||||
method: init?.method ?? "GET",
|
||||
auth: (init?.headers as Record<string, string> | undefined)?.authorization ?? null,
|
||||
});
|
||||
const hit = Object.entries(routes).find(([path]) => u.includes(path));
|
||||
if (!hit) return new Response("{}", { status: 404 });
|
||||
return new Response(JSON.stringify(hit[1].body), { status: hit[1].status });
|
||||
}) as typeof fetch;
|
||||
return { impl, calls };
|
||||
}
|
||||
|
||||
test.beforeEach(() => {
|
||||
process.env.CONDUCTOR_HUB_URL = "http://hub.test:7910";
|
||||
process.env.CONDUCTOR_HUB_TOKEN = "tok-secreto";
|
||||
});
|
||||
|
||||
test.after(() => {
|
||||
delete process.env.CONDUCTOR_HUB_URL;
|
||||
delete process.env.CONDUCTOR_HUB_TOKEN;
|
||||
});
|
||||
|
||||
test("snapshot: runners e tasks sanitizados (whitelist — token do runner NUNCA passa)", async () => {
|
||||
const { impl, calls } = fakeHub({
|
||||
"/v1/runners": { status: 200, body: RUNNERS },
|
||||
"/v1/tasks": { status: 200, body: TASKS },
|
||||
});
|
||||
const snap = await getFleetSnapshot({ fetchImpl: impl });
|
||||
assert.equal(snap.offline, false);
|
||||
assert.deepEqual(snap.runners, [
|
||||
{ id: "r_1", name: "devbox", clis: ["claude", "codex"], online: true, draining: false },
|
||||
]);
|
||||
assert.equal(snap.tasks.length, 2);
|
||||
assert.deepEqual(snap.tasks[0], {
|
||||
id: "t_1",
|
||||
status: "completed",
|
||||
mode: "solo",
|
||||
repo: "https://git.x/repo",
|
||||
runner: "r_1",
|
||||
summary: "feito",
|
||||
branch: "task/t_1",
|
||||
error: null,
|
||||
updated_at: "2026-07-22T00:01:00Z",
|
||||
});
|
||||
assert.ok(!JSON.stringify(snap).includes("NUNCA-VAZAR"), "token de runner não vaza");
|
||||
assert.ok(!JSON.stringify(snap).includes("tok-secreto"), "token do hub não vaza");
|
||||
assert.equal(calls.every((c) => c.auth === "Bearer tok-secreto"), true, "proxy autentica no hub");
|
||||
});
|
||||
|
||||
test("snapshot: hub fora do ar → degradado {offline:true} sem lançar", async () => {
|
||||
const failing = (async () => {
|
||||
throw new Error("ECONNREFUSED");
|
||||
}) as unknown as typeof fetch;
|
||||
const snap = await getFleetSnapshot({ fetchImpl: failing });
|
||||
assert.deepEqual(snap, { offline: true, runners: [], tasks: [] });
|
||||
});
|
||||
|
||||
test("snapshot: env ausente → degradado sem fetch", async () => {
|
||||
delete process.env.CONDUCTOR_HUB_URL;
|
||||
const { impl, calls } = fakeHub({});
|
||||
const snap = await getFleetSnapshot({ fetchImpl: impl });
|
||||
assert.equal(snap.offline, true);
|
||||
assert.equal(calls.length, 0);
|
||||
});
|
||||
|
||||
test("detalhe: manifest e council passam; spec.prompt vem; campos fora da whitelist não", async () => {
|
||||
const { impl } = fakeHub({ "/v1/tasks/t_2": { status: 200, body: TASKS[1] } });
|
||||
const detail = await getConductorTaskDetail("t_2", { fetchImpl: impl });
|
||||
assert.ok(detail);
|
||||
assert.equal(detail!.id, "t_2");
|
||||
assert.equal(detail!.prompt, "outra");
|
||||
assert.deepEqual(detail!.council, { candidate_task_ids: ["t_2a", "t_2b"] });
|
||||
assert.equal(detail!.base_ref, "main");
|
||||
});
|
||||
|
||||
test("detalhe: 404 do hub → null", async () => {
|
||||
const { impl } = fakeHub({});
|
||||
assert.equal(await getConductorTaskDetail("t_x", { fetchImpl: impl }), null);
|
||||
});
|
||||
|
||||
test("cancelar: POST no hub e repassa o status", async () => {
|
||||
const { impl, calls } = fakeHub({ "/v1/tasks/t_1/cancel": { status: 200, body: { ok: true } } });
|
||||
const r = await cancelConductorTask("t_1", { fetchImpl: impl });
|
||||
assert.deepEqual(r, { ok: true, status: 200 });
|
||||
assert.equal(calls[0].method, "POST");
|
||||
const miss = await cancelConductorTask("t_zzz", { fetchImpl: fakeHub({}).impl });
|
||||
assert.deepEqual(miss, { ok: false, status: 404 });
|
||||
});
|
||||
@@ -1,35 +0,0 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
// Componentes React são cobertos pelo vitest-ui (advisory) + dashboard-typecheck;
|
||||
// este source-scan trava os invariantes de segurança/UX que revisão nenhuma pode perder.
|
||||
const CLIENT = "src/app/(dashboard)/dashboard/conductor/ConductorPageClient.tsx";
|
||||
const PAGE = "src/app/(dashboard)/dashboard/conductor/page.tsx";
|
||||
|
||||
test("client: poll com setInterval + clearInterval; fala SÓ com /api/conductor (nunca com o hub)", () => {
|
||||
const src = fs.readFileSync(path.join(process.cwd(), CLIENT), "utf8");
|
||||
assert.match(src, /"use client"/);
|
||||
assert.match(src, /setInterval\(/);
|
||||
assert.match(src, /clearInterval\(/);
|
||||
assert.match(src, /\/api\/conductor\/fleet/);
|
||||
assert.ok(!src.includes("CONDUCTOR_HUB"), "nenhuma env do hub no client");
|
||||
assert.ok(!src.includes(":7910"), "nenhum endereço de hub hardcoded no client");
|
||||
});
|
||||
|
||||
test("client: cancelar é destrutivo → ConfirmModal antes do POST", () => {
|
||||
const src = fs.readFileSync(path.join(process.cwd(), CLIENT), "utf8");
|
||||
assert.match(src, /ConfirmModal/);
|
||||
const confirmAt = src.indexOf("<ConfirmModal");
|
||||
const cancelPost = src.indexOf("/cancel");
|
||||
assert.ok(confirmAt > 0 && cancelPost > 0, "ConfirmModal e POST cancel presentes");
|
||||
assert.match(src, /useTranslations\("conductor"\)/, "strings via i18n, namespace conductor");
|
||||
});
|
||||
|
||||
test("page: wrapper fino de servidor com metadata (padrão relay)", () => {
|
||||
const src = fs.readFileSync(path.join(process.cwd(), PAGE), "utf8");
|
||||
assert.match(src, /export const metadata/);
|
||||
assert.match(src, /ConductorPageClient/);
|
||||
assert.ok(!src.includes('"use client"'), "page.tsx é server component fino");
|
||||
});
|
||||
@@ -1,23 +0,0 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
// Padrão budget-route-auth: prova pelo fonte que o gate de auth vem ANTES de qualquer uso do proxy.
|
||||
const ROUTES = [
|
||||
"src/app/api/conductor/fleet/route.ts",
|
||||
"src/app/api/conductor/tasks/[id]/route.ts",
|
||||
"src/app/api/conductor/tasks/[id]/cancel/route.ts",
|
||||
];
|
||||
|
||||
for (const route of ROUTES) {
|
||||
test(`${route}: requireManagementAuth antes do proxy ao hub`, () => {
|
||||
const src = fs.readFileSync(path.join(process.cwd(), route), "utf8");
|
||||
const authAt = src.indexOf("requireManagementAuth(");
|
||||
assert.ok(authAt > 0, "handler chama requireManagementAuth");
|
||||
assert.match(src, /if \(authError\) return authError;/, "curto-circuito no erro de auth");
|
||||
const proxyAt = src.search(/getFleetSnapshot\(|getConductorTaskDetail\(|cancelConductorTask\(/);
|
||||
assert.ok(proxyAt > authAt, "proxy ao hub só depois do gate de auth");
|
||||
assert.ok(!src.includes("CONDUCTOR_HUB_TOKEN"), "token nunca manuseado na rota (vive no hubProxy)");
|
||||
});
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
const { REGISTRY } = await import("../../open-sse/config/providerRegistry.ts");
|
||||
const { DefaultExecutor, getExecutor } = await import("../../open-sse/executors/index.ts");
|
||||
const { PROVIDER_ENDPOINTS } = await import("../../src/shared/constants/config.ts");
|
||||
const { isValidModel } = await import("../../src/shared/constants/models.ts");
|
||||
const { AGGREGATOR_PROVIDER_IDS } = await import("../../src/shared/constants/providers.ts");
|
||||
const { APIKEY_PROVIDERS } = await import("../../src/shared/constants/providers/apikey/index.ts");
|
||||
|
||||
const providers = [
|
||||
["zylo-api", "zylo", "https://api.zyloai.net/v1/chat/completions"],
|
||||
["unorouter", "unorouter", "https://api.unorouter.com/v1/chat/completions"],
|
||||
["poolside", "poolside", "https://inference.poolside.ai/v1/chat/completions"],
|
||||
["fastrouter", "fastrouter", "https://api.fastrouter.ai/api/v1/chat/completions"],
|
||||
["anyapi", "anyapi", "https://api.anyapi.ai/v1/chat/completions"],
|
||||
["electronhub", "electronhub", "https://api.electronhub.ai/v1/chat/completions"],
|
||||
["llmgateway", "llmgateway", "https://api.llmgateway.io/v1/chat/completions"],
|
||||
["llm-kiwi", "llmkiwi", "https://api.llm.kiwi/v1/chat/completions"],
|
||||
] as const;
|
||||
|
||||
for (const [id, alias, endpoint] of providers) {
|
||||
test(`${id} is wired through registry, metadata, endpoint and default executor`, () => {
|
||||
const registry = REGISTRY[id];
|
||||
const metadata = APIKEY_PROVIDERS[id];
|
||||
|
||||
assert.ok(registry);
|
||||
assert.ok(metadata);
|
||||
assert.equal(registry.id, id);
|
||||
assert.equal(registry.alias, alias);
|
||||
assert.equal(registry.baseUrl, endpoint);
|
||||
assert.equal(PROVIDER_ENDPOINTS[id], endpoint);
|
||||
assert.equal(metadata.id, id);
|
||||
assert.equal(metadata.alias, alias);
|
||||
assert.equal(metadata.hasFree, true);
|
||||
assert.equal(metadata.passthroughModels, true);
|
||||
assert.ok(typeof metadata.freeNote === "string" && metadata.freeNote.length > 0);
|
||||
assert.ok(getExecutor(id) instanceof DefaultExecutor);
|
||||
assert.equal(isValidModel(id, "future/live-catalog-model"), true);
|
||||
assert.equal(isValidModel(alias, "future/live-catalog-model"), true);
|
||||
});
|
||||
}
|
||||
|
||||
test("gateway providers are classified as aggregators while direct Poolside is not", () => {
|
||||
const gatewayIds = providers.map(([id]) => id).filter((id) => id !== "poolside");
|
||||
for (const id of gatewayIds) assert.equal(AGGREGATOR_PROVIDER_IDS.has(id), true);
|
||||
assert.equal(AGGREGATOR_PROVIDER_IDS.has("poolside"), false);
|
||||
});
|
||||
|
||||
test("LLM.Kiwi statically exposes only the confirmed Free plan models", () => {
|
||||
assert.deepEqual(REGISTRY["llm-kiwi"].models, [
|
||||
{ id: "auto", name: "Auto" },
|
||||
{ id: "hrLLM", name: "hrLLM" },
|
||||
]);
|
||||
});
|
||||
@@ -1,51 +0,0 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { literouterProvider } from "../../open-sse/config/providers/registry/literouter/index.ts";
|
||||
import { meganovaAiProvider } from "../../open-sse/config/providers/registry/meganova-ai/index.ts";
|
||||
import { mnnAiProvider } from "../../open-sse/config/providers/registry/mnn-ai/index.ts";
|
||||
import type { RegistryEntry } from "../../open-sse/config/providers/shared.ts";
|
||||
|
||||
const providers: Array<{
|
||||
entry: RegistryEntry;
|
||||
id: string;
|
||||
chatUrl: string;
|
||||
modelsUrl: string;
|
||||
}> = [
|
||||
{
|
||||
entry: literouterProvider,
|
||||
id: "literouter",
|
||||
chatUrl: "https://api.literouter.com/v1/chat/completions",
|
||||
modelsUrl: "https://api.literouter.com/v1/models",
|
||||
},
|
||||
{
|
||||
entry: mnnAiProvider,
|
||||
id: "mnn-ai",
|
||||
chatUrl: "https://api.mnnai.ru/v1/chat/completions",
|
||||
modelsUrl: "https://api.mnnai.ru/v1/models",
|
||||
},
|
||||
{
|
||||
entry: meganovaAiProvider,
|
||||
id: "meganova-ai",
|
||||
chatUrl: "https://api.meganova.ai/v1/chat/completions",
|
||||
modelsUrl: "https://api.meganova.ai/v1/models",
|
||||
},
|
||||
];
|
||||
|
||||
for (const { entry, id, chatUrl, modelsUrl } of providers) {
|
||||
test(`${id} uses an OpenAI-compatible Bearer registry entry`, () => {
|
||||
assert.equal(entry.id, id);
|
||||
assert.equal(entry.alias, id);
|
||||
assert.equal(entry.format, "openai");
|
||||
assert.equal(entry.executor, "default");
|
||||
assert.equal(entry.authType, "apikey");
|
||||
assert.equal(entry.authHeader, "bearer");
|
||||
assert.equal(entry.baseUrl, chatUrl);
|
||||
assert.equal(entry.modelsUrl, modelsUrl);
|
||||
assert.equal(entry.passthroughModels, true);
|
||||
});
|
||||
|
||||
test(`${id} leaves model discovery to the live upstream catalog`, () => {
|
||||
assert.deepEqual(entry.models, []);
|
||||
});
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { mixlayerProvider } from "../../open-sse/config/providers/registry/mixlayer/index.ts";
|
||||
import { spekaProvider } from "../../open-sse/config/providers/registry/speka/index.ts";
|
||||
import { tokenreplyProvider } from "../../open-sse/config/providers/registry/tokenreply/index.ts";
|
||||
import type { RegistryEntry } from "../../open-sse/config/providers/shared.ts";
|
||||
|
||||
const providers: Array<{
|
||||
entry: RegistryEntry;
|
||||
id: string;
|
||||
chatUrl: string;
|
||||
modelsUrl: string;
|
||||
}> = [
|
||||
{
|
||||
entry: mixlayerProvider,
|
||||
id: "mixlayer",
|
||||
chatUrl: "https://models.mixlayer.ai/v1/chat/completions",
|
||||
modelsUrl: "https://models.mixlayer.ai/v1/models",
|
||||
},
|
||||
{
|
||||
entry: spekaProvider,
|
||||
id: "speka",
|
||||
chatUrl: "https://speka.me/v1/chat/completions",
|
||||
modelsUrl: "https://speka.me/v1/models",
|
||||
},
|
||||
{
|
||||
entry: tokenreplyProvider,
|
||||
id: "tokenreply",
|
||||
chatUrl: "https://api.tokenreply.com/v1/chat/completions",
|
||||
modelsUrl: "https://api.tokenreply.com/v1/models",
|
||||
},
|
||||
];
|
||||
|
||||
test("Wave 2-B providers expose OpenAI-compatible Bearer registries", () => {
|
||||
for (const { entry, id, chatUrl, modelsUrl } of providers) {
|
||||
assert.equal(entry.id, id);
|
||||
assert.equal(entry.alias, id);
|
||||
assert.equal(entry.format, "openai");
|
||||
assert.equal(entry.executor, "default");
|
||||
assert.equal(entry.authType, "apikey");
|
||||
assert.equal(entry.authHeader, "bearer");
|
||||
assert.equal(entry.baseUrl, chatUrl);
|
||||
assert.equal(entry.modelsUrl, modelsUrl);
|
||||
assert.equal(entry.passthroughModels, true);
|
||||
assert.ok(Array.isArray(entry.models));
|
||||
}
|
||||
});
|
||||
|
||||
test("Mixlayer seeds only its documented free model", () => {
|
||||
assert.deepEqual(
|
||||
mixlayerProvider.models.map((model) => model.id),
|
||||
["qwen/qwen3.5-4b-free"]
|
||||
);
|
||||
});
|
||||
|
||||
test("Speka and TokenReply rely on live model catalogs without invented models", () => {
|
||||
assert.deepEqual(spekaProvider.models, []);
|
||||
assert.deepEqual(tokenreplyProvider.models, []);
|
||||
});
|
||||
@@ -1,58 +0,0 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { cloudcodeOneProvider } from "../../open-sse/config/providers/registry/cloudcode-one/index.ts";
|
||||
import { dxntProvider } from "../../open-sse/config/providers/registry/dxnt/index.ts";
|
||||
import { yoloAutoProvider } from "../../open-sse/config/providers/registry/yolo-auto/index.ts";
|
||||
import type { RegistryEntry } from "../../open-sse/config/providers/shared.ts";
|
||||
|
||||
const providers: Array<{
|
||||
entry: RegistryEntry;
|
||||
id: string;
|
||||
chatUrl: string;
|
||||
modelsUrl: string;
|
||||
modelIds: string[];
|
||||
}> = [
|
||||
{
|
||||
entry: yoloAutoProvider,
|
||||
id: "yolo-auto",
|
||||
chatUrl: "https://yolo-auto.com/v1/chat/completions",
|
||||
modelsUrl: "https://yolo-auto.com/v1/models",
|
||||
modelIds: ["qwen3.6-35b-a3b"],
|
||||
},
|
||||
{
|
||||
entry: dxntProvider,
|
||||
id: "dxnt",
|
||||
chatUrl: "https://www.dxnt.com/v1/chat/completions",
|
||||
modelsUrl: "https://www.dxnt.com/v1/models",
|
||||
modelIds: [],
|
||||
},
|
||||
{
|
||||
entry: cloudcodeOneProvider,
|
||||
id: "cloudcode-one",
|
||||
chatUrl: "https://api.cloudcode.one/v1/chat/completions",
|
||||
modelsUrl: "https://api.cloudcode.one/v1/models",
|
||||
modelIds: ["glm-4.7-flash", "glm-4.6v-flash"],
|
||||
},
|
||||
];
|
||||
|
||||
for (const { entry, id, chatUrl, modelsUrl, modelIds } of providers) {
|
||||
test(`${id} uses the standard OpenAI-compatible API-key registry shape`, () => {
|
||||
assert.equal(entry.id, id);
|
||||
assert.equal(entry.alias, id);
|
||||
assert.equal(entry.format, "openai");
|
||||
assert.equal(entry.executor, "default");
|
||||
assert.equal(entry.authType, "apikey");
|
||||
assert.equal(entry.authHeader, "bearer");
|
||||
assert.equal(entry.baseUrl, chatUrl);
|
||||
assert.equal(entry.modelsUrl, modelsUrl);
|
||||
assert.equal(entry.passthroughModels, true);
|
||||
});
|
||||
|
||||
test(`${id} seeds only the audited model identifiers`, () => {
|
||||
assert.deepEqual(
|
||||
(entry.models ?? []).map((model) => model.id),
|
||||
modelIds
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
const { REGISTRY } = await import("../../open-sse/config/providerRegistry.ts");
|
||||
const { DefaultExecutor, getExecutor } = await import("../../open-sse/executors/index.ts");
|
||||
const { PROVIDER_ENDPOINTS } = await import("../../src/shared/constants/config.ts");
|
||||
const { isValidModel } = await import("../../src/shared/constants/models.ts");
|
||||
const { AGGREGATOR_PROVIDER_IDS } = await import("../../src/shared/constants/providers.ts");
|
||||
const { APIKEY_PROVIDERS } = await import("../../src/shared/constants/providers/apikey/index.ts");
|
||||
|
||||
const providers = [
|
||||
["literouter", "https://api.literouter.com/v1/chat/completions", []],
|
||||
["mnn-ai", "https://api.mnnai.ru/v1/chat/completions", []],
|
||||
["meganova-ai", "https://api.meganova.ai/v1/chat/completions", []],
|
||||
["mixlayer", "https://models.mixlayer.ai/v1/chat/completions", ["qwen/qwen3.5-4b-free"]],
|
||||
["speka", "https://speka.me/v1/chat/completions", []],
|
||||
["tokenreply", "https://api.tokenreply.com/v1/chat/completions", []],
|
||||
["yolo-auto", "https://yolo-auto.com/v1/chat/completions", ["qwen3.6-35b-a3b"]],
|
||||
["dxnt", "https://www.dxnt.com/v1/chat/completions", []],
|
||||
[
|
||||
"cloudcode-one",
|
||||
"https://api.cloudcode.one/v1/chat/completions",
|
||||
["glm-4.7-flash", "glm-4.6v-flash"],
|
||||
],
|
||||
] as const;
|
||||
|
||||
for (const [id, endpoint, modelIds] of providers) {
|
||||
test(`${id} is wired through registry, metadata, endpoint and default executor`, () => {
|
||||
const registry = REGISTRY[id];
|
||||
const metadata = APIKEY_PROVIDERS[id];
|
||||
|
||||
assert.ok(registry);
|
||||
assert.ok(metadata);
|
||||
assert.equal(registry.id, id);
|
||||
assert.equal(registry.alias, id);
|
||||
assert.equal(registry.baseUrl, endpoint);
|
||||
assert.equal(PROVIDER_ENDPOINTS[id], endpoint);
|
||||
assert.equal(metadata.id, id);
|
||||
assert.equal(metadata.alias, id);
|
||||
assert.equal(metadata.hasFree, true);
|
||||
assert.equal(metadata.passthroughModels, true);
|
||||
assert.ok(typeof metadata.freeNote === "string" && metadata.freeNote.length > 0);
|
||||
assert.equal(AGGREGATOR_PROVIDER_IDS.has(id), true);
|
||||
assert.ok(getExecutor(id) instanceof DefaultExecutor);
|
||||
assert.equal(isValidModel(id, "future/live-catalog-model"), true);
|
||||
assert.deepEqual(
|
||||
registry.models.map((model) => model.id),
|
||||
modelIds
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
test("risk-sensitive metadata preserves the audited quota qualifications", () => {
|
||||
assert.match(APIKEY_PROVIDERS["mnn-ai"].apiHint ?? "", /jurisdiction.*privacy/i);
|
||||
assert.match(APIKEY_PROVIDERS["meganova-ai"].freeNote ?? "", /per-model quotas/i);
|
||||
assert.match(APIKEY_PROVIDERS["meganova-ai"].freeNote ?? "", /paid overage/i);
|
||||
assert.match(APIKEY_PROVIDERS.tokenreply.freeNote ?? "", /no fixed global free quota/i);
|
||||
assert.match(APIKEY_PROVIDERS["yolo-auto"].freeNote ?? "", /no numeric daily quota/i);
|
||||
assert.match(APIKEY_PROVIDERS["cloudcode-one"].freeNote ?? "", /credit or a coupon/i);
|
||||
});
|
||||
@@ -1,48 +0,0 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { chatanywhereProvider } from "../../open-sse/config/providers/registry/chatanywhere/index.ts";
|
||||
import { ofoxaiProvider } from "../../open-sse/config/providers/registry/ofoxai/index.ts";
|
||||
import { zerolimitaiProvider } from "../../open-sse/config/providers/registry/zerolimitai/index.ts";
|
||||
import type { RegistryEntry } from "../../open-sse/config/providers/shared.ts";
|
||||
|
||||
const providers: Array<{
|
||||
entry: RegistryEntry;
|
||||
id: string;
|
||||
chatUrl: string;
|
||||
modelsUrl: string;
|
||||
}> = [
|
||||
{
|
||||
entry: ofoxaiProvider,
|
||||
id: "ofoxai",
|
||||
chatUrl: "https://api.ofox.ai/v1/chat/completions",
|
||||
modelsUrl: "https://api.ofox.ai/v1/models",
|
||||
},
|
||||
{
|
||||
entry: zerolimitaiProvider,
|
||||
id: "zerolimitai",
|
||||
chatUrl: "https://www.zerolimitai.com/api/v1/chat/completions",
|
||||
modelsUrl: "https://www.zerolimitai.com/api/v1/models",
|
||||
},
|
||||
{
|
||||
entry: chatanywhereProvider,
|
||||
id: "chatanywhere",
|
||||
chatUrl: "https://api.chatanywhere.org/v1/chat/completions",
|
||||
modelsUrl: "https://api.chatanywhere.org/v1/models",
|
||||
},
|
||||
];
|
||||
|
||||
test("Wave 3-A providers expose OpenAI-compatible Bearer registries", () => {
|
||||
for (const { entry, id, chatUrl, modelsUrl } of providers) {
|
||||
assert.equal(entry.id, id);
|
||||
assert.equal(entry.alias, id);
|
||||
assert.equal(entry.format, "openai");
|
||||
assert.equal(entry.executor, "default");
|
||||
assert.equal(entry.authType, "apikey");
|
||||
assert.equal(entry.authHeader, "bearer");
|
||||
assert.equal(entry.baseUrl, chatUrl);
|
||||
assert.equal(entry.modelsUrl, modelsUrl);
|
||||
assert.equal(entry.passthroughModels, true);
|
||||
assert.deepEqual(entry.models, []);
|
||||
}
|
||||
});
|
||||
@@ -1,53 +0,0 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { aurikoProvider } from "../../open-sse/config/providers/registry/auriko/index.ts";
|
||||
import { helyxaiProvider } from "../../open-sse/config/providers/registry/helyxai/index.ts";
|
||||
import { poixeAiProvider } from "../../open-sse/config/providers/registry/poixe-ai/index.ts";
|
||||
import type { RegistryEntry } from "../../open-sse/config/providers/shared.ts";
|
||||
|
||||
const providers: Array<{
|
||||
entry: RegistryEntry;
|
||||
id: string;
|
||||
chatUrl: string;
|
||||
modelsUrl: string;
|
||||
}> = [
|
||||
{
|
||||
entry: helyxaiProvider,
|
||||
id: "helyxai",
|
||||
chatUrl: "https://helyxai.space/v1/chat/completions",
|
||||
modelsUrl: "https://helyxai.space/v1/models",
|
||||
},
|
||||
{
|
||||
entry: aurikoProvider,
|
||||
id: "auriko",
|
||||
chatUrl: "https://api.auriko.ai/v1/chat/completions",
|
||||
modelsUrl: "https://api.auriko.ai/v1/models",
|
||||
},
|
||||
{
|
||||
entry: poixeAiProvider,
|
||||
id: "poixe-ai",
|
||||
chatUrl: "https://api.poixe.com/v1/chat/completions",
|
||||
modelsUrl: "https://api.poixe.com/v1/models",
|
||||
},
|
||||
];
|
||||
|
||||
test("Wave 3-B providers expose OpenAI-compatible Bearer registries", () => {
|
||||
for (const { entry, id, chatUrl, modelsUrl } of providers) {
|
||||
assert.equal(entry.id, id);
|
||||
assert.equal(entry.alias, id);
|
||||
assert.equal(entry.format, "openai");
|
||||
assert.equal(entry.executor, "default");
|
||||
assert.equal(entry.authType, "apikey");
|
||||
assert.equal(entry.authHeader, "bearer");
|
||||
assert.equal(entry.baseUrl, chatUrl);
|
||||
assert.equal(entry.modelsUrl, modelsUrl);
|
||||
assert.equal(entry.passthroughModels, true);
|
||||
}
|
||||
});
|
||||
|
||||
test("Wave 3-B providers rely on live catalogs without invented models", () => {
|
||||
for (const { entry } of providers) {
|
||||
assert.deepEqual(entry.models, []);
|
||||
}
|
||||
});
|
||||
@@ -1,41 +0,0 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { chatOripeProvider } from "../../open-sse/config/providers/registry/chat-oripe/index.ts";
|
||||
import { nagaAiProvider } from "../../open-sse/config/providers/registry/naga-ai/index.ts";
|
||||
import type { RegistryEntry } from "../../open-sse/config/providers/shared.ts";
|
||||
|
||||
const providers: Array<{
|
||||
entry: RegistryEntry;
|
||||
id: string;
|
||||
chatUrl: string;
|
||||
modelsUrl: string;
|
||||
}> = [
|
||||
{
|
||||
entry: nagaAiProvider,
|
||||
id: "naga-ai",
|
||||
chatUrl: "https://api.naga.ac/v1/chat/completions",
|
||||
modelsUrl: "https://api.naga.ac/v1/models",
|
||||
},
|
||||
{
|
||||
entry: chatOripeProvider,
|
||||
id: "chat-oripe",
|
||||
chatUrl: "https://api.oriper.com/v1/chat/completions",
|
||||
modelsUrl: "https://api.oriper.com/v1/models",
|
||||
},
|
||||
];
|
||||
|
||||
test("Wave 3-C providers expose OpenAI-compatible Bearer registries", () => {
|
||||
for (const { entry, id, chatUrl, modelsUrl } of providers) {
|
||||
assert.equal(entry.id, id);
|
||||
assert.equal(entry.alias, id);
|
||||
assert.equal(entry.format, "openai");
|
||||
assert.equal(entry.executor, "default");
|
||||
assert.equal(entry.authType, "apikey");
|
||||
assert.equal(entry.authHeader, "bearer");
|
||||
assert.equal(entry.baseUrl, chatUrl);
|
||||
assert.equal(entry.modelsUrl, modelsUrl);
|
||||
assert.equal(entry.passthroughModels, true);
|
||||
assert.deepEqual(entry.models, []);
|
||||
}
|
||||
});
|
||||
@@ -1,53 +0,0 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
const { REGISTRY } = await import("../../open-sse/config/providerRegistry.ts");
|
||||
const { DefaultExecutor, getExecutor } = await import("../../open-sse/executors/index.ts");
|
||||
const { PROVIDER_ENDPOINTS } = await import("../../src/shared/constants/config.ts");
|
||||
const { isValidModel } = await import("../../src/shared/constants/models.ts");
|
||||
const { AGGREGATOR_PROVIDER_IDS } = await import("../../src/shared/constants/providers.ts");
|
||||
const { APIKEY_PROVIDERS } = await import("../../src/shared/constants/providers/apikey/index.ts");
|
||||
|
||||
const providers = [
|
||||
["ofoxai", "https://api.ofox.ai/v1/chat/completions"],
|
||||
["zerolimitai", "https://www.zerolimitai.com/api/v1/chat/completions"],
|
||||
["chatanywhere", "https://api.chatanywhere.org/v1/chat/completions"],
|
||||
["helyxai", "https://helyxai.space/v1/chat/completions"],
|
||||
["auriko", "https://api.auriko.ai/v1/chat/completions"],
|
||||
["poixe-ai", "https://api.poixe.com/v1/chat/completions"],
|
||||
["naga-ai", "https://api.naga.ac/v1/chat/completions"],
|
||||
["chat-oripe", "https://api.oriper.com/v1/chat/completions"],
|
||||
] as const;
|
||||
|
||||
for (const [id, endpoint] of providers) {
|
||||
test(`${id} is wired through registry, metadata, endpoint and default executor`, () => {
|
||||
const registry = REGISTRY[id];
|
||||
const metadata = APIKEY_PROVIDERS[id];
|
||||
|
||||
assert.ok(registry);
|
||||
assert.ok(metadata);
|
||||
assert.equal(registry.id, id);
|
||||
assert.equal(registry.alias, id);
|
||||
assert.equal(registry.baseUrl, endpoint);
|
||||
assert.equal(PROVIDER_ENDPOINTS[id], endpoint);
|
||||
assert.equal(metadata.id, id);
|
||||
assert.equal(metadata.alias, id);
|
||||
assert.equal(metadata.hasFree, true);
|
||||
assert.equal(metadata.passthroughModels, true);
|
||||
assert.ok(typeof metadata.freeNote === "string" && metadata.freeNote.length > 0);
|
||||
assert.equal(AGGREGATOR_PROVIDER_IDS.has(id), true);
|
||||
assert.ok(getExecutor(id) instanceof DefaultExecutor);
|
||||
assert.equal(isValidModel(id, "future/live-catalog-model"), true);
|
||||
assert.deepEqual(registry.models, []);
|
||||
});
|
||||
}
|
||||
|
||||
test("Wave 3 metadata preserves the audited legal, quota and privacy warnings", () => {
|
||||
assert.match(APIKEY_PROVIDERS.chatanywhere.freeNote ?? "", /commercial traffic/i);
|
||||
assert.match(APIKEY_PROVIDERS.zerolimitai.freeNote ?? "", /3 and 7 days/i);
|
||||
assert.match(APIKEY_PROVIDERS.helyxai.freeNote ?? "", /100,000 tokens\/day/i);
|
||||
assert.match(APIKEY_PROVIDERS.auriko.freeNote ?? "", /not a free-token pool/i);
|
||||
assert.match(APIKEY_PROVIDERS["poixe-ai"].freeNote ?? "", /2 RPM\/5 RPD/i);
|
||||
assert.match(APIKEY_PROVIDERS["naga-ai"].freeNote ?? "", /training/i);
|
||||
assert.match(APIKEY_PROVIDERS["chat-oripe"].freeNote ?? "", /unconfirmed/i);
|
||||
});
|
||||
@@ -1,27 +0,0 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { freeinferenceProvider } from "../../open-sse/config/providers/registry/freeinference/index.ts";
|
||||
import {
|
||||
DefaultExecutor,
|
||||
getExecutor,
|
||||
hasSpecializedExecutor,
|
||||
} from "../../open-sse/executors/index.ts";
|
||||
|
||||
test("FreeInference exposes an OpenAI-compatible Bearer registry", () => {
|
||||
assert.equal(freeinferenceProvider.id, "freeinference");
|
||||
assert.equal(freeinferenceProvider.alias, "freeinference");
|
||||
assert.equal(freeinferenceProvider.format, "openai");
|
||||
assert.equal(freeinferenceProvider.executor, "default");
|
||||
assert.equal(freeinferenceProvider.authType, "apikey");
|
||||
assert.equal(freeinferenceProvider.authHeader, "bearer");
|
||||
assert.equal(freeinferenceProvider.baseUrl, "https://freeinference.org/v1/chat/completions");
|
||||
assert.equal(freeinferenceProvider.modelsUrl, "https://freeinference.org/v1/models");
|
||||
assert.deepEqual(freeinferenceProvider.models, []);
|
||||
assert.equal(freeinferenceProvider.passthroughModels, true);
|
||||
});
|
||||
|
||||
test("FreeInference uses DefaultExecutor without specialized behavior", () => {
|
||||
assert.equal(hasSpecializedExecutor("freeinference"), false);
|
||||
assert.ok(getExecutor("freeinference") instanceof DefaultExecutor);
|
||||
});
|
||||
@@ -1,27 +0,0 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { freeAiProvider } from "../../open-sse/config/providers/registry/free-ai/index.ts";
|
||||
import {
|
||||
DefaultExecutor,
|
||||
getExecutor,
|
||||
hasSpecializedExecutor,
|
||||
} from "../../open-sse/executors/index.ts";
|
||||
|
||||
test("Free.ai exposes its exact OpenAI-compatible endpoint and live catalog", () => {
|
||||
assert.equal(freeAiProvider.id, "free-ai");
|
||||
assert.equal(freeAiProvider.alias, "free-ai");
|
||||
assert.equal(freeAiProvider.format, "openai");
|
||||
assert.equal(freeAiProvider.executor, "default");
|
||||
assert.equal(freeAiProvider.authType, "apikey");
|
||||
assert.equal(freeAiProvider.authHeader, "bearer");
|
||||
assert.equal(freeAiProvider.baseUrl, "https://api.free.ai/v1/chat/");
|
||||
assert.equal(freeAiProvider.modelsUrl, "https://api.free.ai/v1/models");
|
||||
assert.deepEqual(freeAiProvider.models, []);
|
||||
assert.equal(freeAiProvider.passthroughModels, true);
|
||||
});
|
||||
|
||||
test("Free.ai uses DefaultExecutor without a specialized executor", () => {
|
||||
assert.ok(getExecutor("free-ai") instanceof DefaultExecutor);
|
||||
assert.equal(hasSpecializedExecutor("free-ai"), false);
|
||||
});
|
||||
@@ -1,64 +0,0 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { deriveConfigFromRegistryModelsUrl } from "../../src/app/api/providers/[id]/models/discoveryConfig.ts";
|
||||
|
||||
const { REGISTRY } = await import("../../open-sse/config/providerRegistry.ts");
|
||||
const { DefaultExecutor, getExecutor, hasSpecializedExecutor } =
|
||||
await import("../../open-sse/executors/index.ts");
|
||||
const { PROVIDER_ENDPOINTS } = await import("../../src/shared/constants/config.ts");
|
||||
const { isValidModel } = await import("../../src/shared/constants/models.ts");
|
||||
const { AGGREGATOR_PROVIDER_IDS } = await import("../../src/shared/constants/providers.ts");
|
||||
const { APIKEY_PROVIDERS } = await import("../../src/shared/constants/providers/apikey/index.ts");
|
||||
|
||||
const providers = [
|
||||
["freeinference", "https://freeinference.org/v1/chat/completions"],
|
||||
["free-ai", "https://api.free.ai/v1/chat/"],
|
||||
] as const;
|
||||
|
||||
for (const [id, endpoint] of providers) {
|
||||
test(`${id} is fully wired without a specialized executor`, () => {
|
||||
const registry = REGISTRY[id];
|
||||
const metadata = APIKEY_PROVIDERS[id];
|
||||
|
||||
assert.ok(registry);
|
||||
assert.ok(metadata);
|
||||
assert.equal(registry.id, id);
|
||||
assert.equal(registry.alias, id);
|
||||
assert.equal(registry.baseUrl, endpoint);
|
||||
assert.equal(PROVIDER_ENDPOINTS[id], endpoint);
|
||||
assert.equal(metadata.id, id);
|
||||
assert.equal(metadata.alias, id);
|
||||
assert.equal(metadata.hasFree, true);
|
||||
assert.equal(metadata.passthroughModels, true);
|
||||
assert.equal(AGGREGATOR_PROVIDER_IDS.has(id), true);
|
||||
assert.equal(hasSpecializedExecutor(id), false);
|
||||
const executor = getExecutor(id);
|
||||
assert.ok(executor instanceof DefaultExecutor);
|
||||
assert.equal(executor.buildUrl("live-model", false), endpoint);
|
||||
assert.equal(isValidModel(id, "future/live-catalog-model"), true);
|
||||
assert.deepEqual(registry.models, []);
|
||||
});
|
||||
}
|
||||
|
||||
test("Wave 4 model discovery accepts both public catalog response envelopes", () => {
|
||||
const freeInferenceDiscovery = deriveConfigFromRegistryModelsUrl("freeinference");
|
||||
const freeAiDiscovery = deriveConfigFromRegistryModelsUrl("free-ai");
|
||||
|
||||
assert.ok(freeInferenceDiscovery);
|
||||
assert.ok(freeAiDiscovery);
|
||||
assert.deepEqual(freeInferenceDiscovery.parseResponse({ data: [{ id: "glm-5.1" }] }), [
|
||||
{ id: "glm-5.1" },
|
||||
]);
|
||||
assert.deepEqual(freeAiDiscovery.parseResponse({ models: [{ id: "qwen7b" }] }), [
|
||||
{ id: "qwen7b" },
|
||||
]);
|
||||
});
|
||||
|
||||
test("Wave 4 metadata preserves approval, logging and overage warnings", () => {
|
||||
assert.match(APIKEY_PROVIDERS.freeinference.freeNote ?? "", /manual approval/i);
|
||||
assert.match(APIKEY_PROVIDERS.freeinference.apiHint ?? "", /logging/i);
|
||||
assert.match(APIKEY_PROVIDERS["free-ai"].freeNote ?? "", /30,000 tokens\/day/i);
|
||||
assert.match(APIKEY_PROVIDERS["free-ai"].freeNote ?? "", /premium external models are paid/i);
|
||||
assert.match(APIKEY_PROVIDERS["free-ai"].apiHint ?? "", /\/v1\/chat\//i);
|
||||
});
|
||||
@@ -1,15 +0,0 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
const { isValidModel } = await import("../../src/shared/constants/models.ts");
|
||||
|
||||
test("passthrough providers accept live model ids through both provider id and alias", () => {
|
||||
for (const [id, alias] of [
|
||||
["zylo-api", "zylo"],
|
||||
["llm-kiwi", "llmkiwi"],
|
||||
["freetheai", "fta"],
|
||||
] as const) {
|
||||
assert.equal(isValidModel(id, "future/live-catalog-model"), true);
|
||||
assert.equal(isValidModel(alias, "future/live-catalog-model"), true);
|
||||
}
|
||||
});
|
||||
@@ -1,57 +0,0 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import test from "node:test";
|
||||
|
||||
const REPO_ROOT = join(import.meta.dirname, "..", "..");
|
||||
|
||||
/**
|
||||
* Regression guards for the CodeQL alerts triaged on 2026-08-12.
|
||||
*
|
||||
* Both are source-level invariants rather than behavioral round-trips: the functions they
|
||||
* protect are module-private (`decodeXmlText`) or only reachable through a live upstream
|
||||
* handshake (`tinycms` nonce), so the guard asserts the property on the source itself.
|
||||
*/
|
||||
|
||||
test("decodeXmlText decodes & last so encoded entities do not double-unescape", () => {
|
||||
const source = readFileSync(
|
||||
join(REPO_ROOT, "open-sse/vendor/codex-chatgpt-web/adapters/chatgpt-web/environment.ts"),
|
||||
"utf8"
|
||||
);
|
||||
const body = /function decodeXmlText\(value: string\): string \{([\s\S]*?)\n\}/.exec(source)?.[1];
|
||||
assert.ok(body, "decodeXmlText not found — update this guard if the helper was renamed");
|
||||
|
||||
const order = [...body.matchAll(/replaceAll\("(&[^"]+;)"/g)].map((match) => match[1]);
|
||||
assert.ok(order.length >= 2, `expected several entity replacements, got ${order.length}`);
|
||||
assert.equal(
|
||||
order.at(-1),
|
||||
"&",
|
||||
`"&" must be the LAST entity decoded, otherwise "&quot;" decodes to '"' instead ` +
|
||||
`of the literal """. Current order: ${order.join(" -> ")}`
|
||||
);
|
||||
|
||||
// Mirror the implementation to document the property the ordering buys us.
|
||||
const decode = (value: string) =>
|
||||
order.reduce((acc, entity) => {
|
||||
const plain = { "<": "<", ">": ">", """: '"', "'": "'", "&": "&" }[entity];
|
||||
return plain === undefined ? acc : acc.replaceAll(entity, plain);
|
||||
}, value);
|
||||
assert.equal(decode("&quot;"), """);
|
||||
assert.equal(decode("&#39;"), "'");
|
||||
assert.equal(decode("&lt;"), "<");
|
||||
});
|
||||
|
||||
test("tinycms signs its anti-replay nonce with a CSPRNG, never Math.random", () => {
|
||||
const source = readFileSync(join(REPO_ROOT, "open-sse/executors/tinycms.ts"), "utf8");
|
||||
|
||||
assert.match(
|
||||
source,
|
||||
/const nonceJs = randomUUID\(\)/,
|
||||
"the tinycms nonce must come from node:crypto randomUUID"
|
||||
);
|
||||
assert.doesNotMatch(
|
||||
source,
|
||||
/Math\.random\(\)/,
|
||||
"Math.random() is not a CSPRNG — the nonce is signed into the anti-replay payload"
|
||||
);
|
||||
});
|
||||
@@ -65,7 +65,6 @@ test("primary sidebar items place limits after cache", () => {
|
||||
"cli-agents",
|
||||
"acp-agents",
|
||||
"cloud-agents",
|
||||
"conductor",
|
||||
"agent-bridge",
|
||||
"traffic-inspector",
|
||||
"discovery",
|
||||
|
||||
Reference in New Issue
Block a user