Compare commits

...

27 Commits

Author SHA1 Message Date
N123 Project
6cd4d38e21 fix(m365): BizChat invocation shape drift + HAR-import UX + Antigravity alias note (#11069)
5 — M365 Copilot (BizChat) individual/consumer path — 3 itens: (1) forma de invocação do #10718 derivou de novo (2026-08-21 capture): optionsSets 14→34, allowedMessageTypes 6→30, tone "magic"→"Magic", plugins []→[{BingWebSearch}], disconnectBehavior em todos os tiers, +8 keys de clientInfo; verificado contra conta real com round-trip WebSocket (ping-then-close → resposta real). (2) Aviso sobre o alias Antigravity gemini-3.1-pro-high ainda não publicado (3.8.49 pré-data). (3) Botão "Import .har file" no modal de credencial M365.

Conflito resolvido em copilot-m365-frames.ts (board vs release tip): mantive o forwarding de opts.plugins/toolChoice/customInstructions do HEAD com os NOVOS defaults da captura (BingWebSearch builtin, tone "Magic"). Alinhei 3 testes pré-existentes que afirmavam o contrato antigo (m365-bizchat-frames-4042 clientInfo, m365-tone-model-variants tone, copilot-m365-tool-calls plugins) — propagação de contrato, não mascaramento. Rebaselinei AddApiKeyModal 1073→1080 (crescimento próprio da parte 3, ~Har import button) com anotação.

Validação: typecheck limpo, 142/142 testes m365/copilot verdes, changelog-integrit/file-size/eslint OK.
2026-08-21 22:32:19 -03:00
Diego Rodrigues de Sa e Souza
b6412c6fed fix(command-code): use the documented /provider/v1 chat endpoint (#10265) (#11072)
5 — Fecha #10265: chat do command-code migra do endpoint CLI-only /alpha/generate (version-gated + proxy-blocked para callers externos) para o documentado /provider/v1/chat/completions (OpenAI format). Removido o envelope CLI reverse-engineered (config/memory/taste/skills + headers CLI-impersonation), substituído por passthrough OpenAI plano com normalização de model id vendor-prefixed (#10809), clamp de max_tokens (#5166), sanitização de reasoning_effort. commandCode.ts 1037→171 linhas.
Validado no worktree board sobre tip: typecheck:core limpo; 175/175 testes focados (command-code executor/vision/usage/maxtokens/user-array/validation-specialty/responses-handler/provider-models-scoping); changelog-integrity/file-size/complexity/cognitive todos OK. TDD RED→GREEN documentado.
2026-08-21 22:16:45 -03:00
Nguyen Thanh Dat
e06f8b7ec0 feat(api): flag a pinned account on /v1/combos steps without leaking the id (#11076)
5 — /v1/combos strip connectionId de propósito; dois passos pinando contas DIFERENTES do mesmo provider viram objetos byte-idênticos e um cliente conclui que não há failover. Adiciona accountPinned (boolean derivado do connectionId, nunca vaza o id) em cada passo model; sempre true/false, nunca em combo-ref. Sem rota/schema/mudança de caller. TDD. Fecha #10968.
2026-08-21 22:06:43 -03:00
Diego Rodrigues de Sa e Souza
5a60a46e22 fix: deprecate blackbox provider since api.blackbox.ai returns 404 (#10997) (#11074)
5 — api.blackbox.ai retorna 404 (curl-verificado); espelha o precedente galadriel: deprecated:true + riskNoticeVariant + subscriptionRisk + deprecationReason (flag de display apenas, não bloqueia registro/execução). TDD + 171 testes irmãos. Fecha #10997.
2026-08-21 22:06:26 -03:00
Rouzbeh†
7ddbaf69a4 feat(cli): add native Bun backend support and Dockerfile.bun (#11039)
4 — Suporte de backend nativo Bun + Dockerfile.bun multi-stage + fallback dinâmico de driver SQLite (better-sqlite3 prioritário sob Bun, bun:sqlite fallback; Node preservado) + correção de estabilidade do DAST CI smoke.
Validado a fundo (worktree board sobre tip): bun-support 4/4, typecheck:core limpo, dashboard-typecheck OK (220 dentro do baseline), open-sse-typecheck OK (5 pré-existentes), gate de runtime OK sob Node, changelog-integrity OK, file-size/complexity/cognitive/dead-code OK. Verificado que o driver preserva a cadeia Node/falback conforme AGENTS.md; teste bun-support presente. Baselines de typecheck removidos são ratchet honesto (erros não existem mais).
OBS: destravei 2 base-reds do tip neste turno (push direto 7ffa3ef): movi o changelog fragment da #11050 da seção inválida breaking/ para fixes/, e rebaselinei AddApiKeyModal 1067->1073 (crescimento da #11056). Sem isso a #11039 e o resto da fila ficariam vermelhos.
2026-08-21 21:28:43 -03:00
Markus Hartung
7ffa3efaf0 fix(quality): move #11050 changelog fragment to valid fixes/ section + rebaseline AddApiKeyModal (1067->1073, #11056 growth) 2026-08-21 21:25:26 -03:00
Rouzbeh†
7c39e95972 fix(providers): update hailuo-web domain to chat.minimax.io (#11000) (#11055)
5 — Atualiza domínio do provider hailuo-web para chat.minimax.io (#11000). Merge autorizado pelo operador (ordem explícita de prosseguir sem o smoke VPS da Hard Rule #18).
2026-08-21 21:10:10 -03:00
Rouzbeh†
8643e0f57c fix(cli): default limit.context to 128k when unknown in OpenCode configs (#11035, #11032) (#11054)
5 — OpenCode config: limit.context default 128k quando metadata de catálogo desconhecida (#11035/#11032); limit emitido por model entry. TDD, suíte aberta limpa.
2026-08-21 21:06:21 -03:00
Rouzbeh†
7e48be8061 feat(dashboard): trigger key validation on Enter in AddApiKeyModal (#10995) (#11056)
5 — onKeyDown Enter no input de chave da AddApiKeyModal dispara a validação. TDD. Fecha #10995.
2026-08-21 21:06:18 -03:00
Rouzbeh†
9b801b7e09 fix(dashboard): compute unique connection names from array to avoid overwrite (#11033) (#11067)
5 — computeConnectionDefaultName aceita array de nomes existentes e acha o próximo não-conflitante, evitando overwrite de nome de conexão. TDD. Fecha #11033.
2026-08-21 21:06:14 -03:00
Paco Cartones
0ff0490ada test(db): assert resetDbInstance swaps the singleton, WAL mode, and schema_version seed (#10906)
5 — Preenche os 3 test.skip com asserções reais (resetDbInstance troca o singleton preservando a linha no disco, journal_mode WAL, schema_version=1). Além do valor pretendido, o autor redesenhou o setup()/cleanup() do arquivo corrigindo um bug de isolamento pré-existente que eu apontei em review: DATA_DIR/SQLITE_FILE são const de topo de módulo; o cleanup() usava require() CJS que nunca resetava a instância ESM-importada, então os testes 1-4 passavam "por acidente" contra a conexão nunca fechada. Agora: tempDir compartilhado definido antes do primeiro import, resetDbInstance importado via ESM uma vez, handle fechado antes de cada reopen, e o catch{} silencioso removido. 7/7 verdes no arquivo inteiro.
2026-08-21 21:02:30 -03:00
Paco Cartones
f968496cc6 fix(gamification): validate leaderboard limit/offset before the SQLite bind (#11059)
5 — LIMIT negativo = "sem limite" no SQLite: ?limit=-1 retornava a leaderboard inteira em endpoint management autenticado; ceil por Math.min só no upper. Duas camadas: route rejeita não-inteiro/fora de range com 400 (mesmo contrato de parseListLimit), getTopN clampeia como backstop defense-in-depth + exporta LEADERBOARD_MAX_LIMIT. TDD red→green, 5 casos novos, 84/84 suíte gamification. Fecha #11058.
2026-08-21 20:59:57 -03:00
Dizzle
c9775366f9 refactor(providers): dedupe identical opencode-zen/opencode-go model entries (#11051)
5 — Dedupe dos 3 entries byte-idênticos (kimi-k2.7-code, qwen3.5-plus, qwen3.6-plus) entre opencode-zen e opencode-go via OPENCODE_ZEN_GO_SHARED_MODELS (padrão GLM_SHARED_MODELS), frozen e testado por deep-equal. Pure move, sem mudança de comportamento; check:provider-consistency OK (267 entries, 348 providers). TDD 3/3. Follow-up de #11049/#11048. Base-red #9985 inherited.
2026-08-21 20:55:21 -03:00
Dizzle
666e4aaca2 fix(providers): route Muse Spark to the Responses API on opencode-zen too (#11049)
5 — Muse Spark responde só na Responses API; #10874 corrigiu só o provider `opencode`, mas `opencode-zen` (faltava targetFormat em 2 entries) e `opencode-go` (6 entries effort-tier) nunca receberam. Espelha a declaração. freebuff NÃO tocado (backend codebuff.com /chat/completions distinto, verificado). TDD 2/2 + 18/18 irmãos. Fecha #11048. Base-red #9985 inherited.
2026-08-21 20:55:07 -03:00
Dizzle
1c920eb8b8 fix(webhooks): remove 3 declared-but-never-emitted ghost events (#11050)
5 — Remove 3 webhook events declarados mas nunca emitidos (provider.error/recovered, combo.switched): union 7→4, z.enum com 400 em ghost values (era z.string pass-through). Breaking intencional + testado. TDD 3/3, i18n B-pattern (42 __MISSING__). Base-red #9985 inherited.
2026-08-21 20:54:51 -03:00
Dizzle
02a6c3d90b fix(sse): split concatenated tool_call arguments from same-name index collisions (#11043)
5 — Providers que não bumpam index/id em tool calls repetidas do mesmo nome colam N arguments JSON num só ({...}{...}{...}); leitores a jusante pegam só o primeiro e dropam o resto em silêncio. Detecta N objetos concatenados e divide de volta em N tool_calls. TDD 20/20 + 86/86 irmãos. Fecha #11044. Base-red #9985 inherited.
2026-08-21 20:54:48 -03:00
Dizzle
ae2de4511b fix(sse): resolve OpencodeExecutor target format through the provider alias (#11047)
5 — OpencodeExecutor buscava targetFormat com o id bruto do provider em vez do alias público ("oc"), caindo em default openai → corpo Responses no URL chat/completions (400). resolveOpencodeTargetFormat resolve via PROVIDER_ID_TO_ALIAS. TDD 4/4 + 32/32 irmãos, lint/tsc limpos. Fecha #11046. Base-red #9985 inherited.
2026-08-21 20:54:44 -03:00
Diego Rodrigues de Sa e Souza
d01a4ae6cf fix(release): drain v3.8.50 base-reds — build-breaking import, stale provider docs, orphaned suppressions (#11038)
5 — Drain de base-reds v3.8.50 (#9985): corrige build-break do #10939 (import fantasma @/lib/db/connections → reroteado por getProviderConnectionById real), headers Anthropic no fallback GLM, fallthrough do reject de listing CC-compatible, no-auth leak no auto-combo, retry de mesmo-account nos combo live tests, pin onnxruntime-node ~1.24.3; + alignments de teste + gates (env-doc, docs-counts 347→348, openapi floor, i18n vi/pt-BR).

Reconciliado com o tip: 5 conflitos de contagem gerada (SVGs + PROVIDER_REFERENCE — HEAD já em 348, branch em 347 stale) resolvidos ficando com o HEAD. typecheck limpo, gates verdes, 5/5 testes focados (#10939 regressão + utilização), lint limpo nos tocados.

Também rebaselined neste turno via push direto ao tip: config/quality/file-size-baseline.json modelCapabilities.ts 1016→1072 (drift de PRs ja mergeadas, ex: #11034), com changelog fragment — tip estava vermelho nesse gate e travava esta PR.
2026-08-21 20:49:18 -03:00
Markus Hartung
9349af29c4 fix(quality): rebaseline file-size baseline for modelCapabilities.ts drift (1016->1072) 2026-08-21 20:46:43 -03:00
Diego Rodrigues de Sa e Souza
eb4fd74b13 fix(security): close remaining v3.8.50 advisories (batch 2 — 11 findings) (#11040)
5 — Fecha 11 achados restantes das advisories de segurança do v3.8.50 (batch 2), TDD. UNSTABLE é o base-red #9985 já rastreado.
2026-08-21 20:28:16 -03:00
Diego Rodrigues de Sa e Souza
99111f39fb fix(services): isolate probeBeforeSpawn adoption tests on distinct ports to stop order-dependent flake (#10523) (#11061)
5 — Fix do dono com TDD. Estado committed+pushed limpo (hold-vivo cedido por instrução direta do operador).
2026-08-21 20:27:49 -03:00
Diego Rodrigues de Sa e Souza
4e3e53ee4d fix(ssrf): honor local-first provider URL flag in outbound guard (#9123) (#11062)
5 — Fix do dono com TDD. Estado committed+pushed limpo (hold-vivo cedido por instrução direta do operador).
2026-08-21 20:27:46 -03:00
Diego Rodrigues de Sa e Souza
a928fad895 fix(command-code): surface reasoning-only output as content when no text-delta (#10986) (#11063)
5 — Fix do dono com TDD. Estado committed+pushed limpo (hold-vivo cedido por instrução direta do operador).
2026-08-21 20:27:43 -03:00
Diego Rodrigues de Sa e Souza
d61eec63b5 fix(dashboard): treat UncloseAI as a no-auth provider so the connect form no longer forces a fake API key (#8864) (#11064)
5 — Fix do dono com TDD. Estado committed+pushed limpo (hold-vivo cedido por instrução direta do operador).
2026-08-21 20:27:40 -03:00
Diego Rodrigues de Sa e Souza
861ac69e4b fix(providers): validate Dify keys against native /v1/chat-messages endpoint (#11002) (#11065)
5 — Fix do dono com TDD. Estado committed+pushed limpo (hold-vivo cedido por instrução direta do operador).
2026-08-21 20:27:36 -03:00
Diego Rodrigues de Sa e Souza
484cb6e562 fix: add static model catalog for v0-vercel-web web-cookie provider (#10990) (#11066)
5 — Fix do dono com TDD. Estado committed+pushed limpo (hold-vivo cedido por instrução direta do operador).
2026-08-21 20:27:33 -03:00
Diego Rodrigues de Sa e Souza
2ab16d3214 fix(cli): scoping android instrumentation-hook diagnosis to real platform signal (#10028) (#11068)
5 — Fix do dono com TDD. Estado committed+pushed limpo (hold-vivo cedido por instrução direta do operador).
2026-08-21 20:27:29 -03:00
207 changed files with 3662 additions and 3042 deletions

View File

@@ -46,6 +46,7 @@ jobs:
env:
PORT: "20128"
INJECTION_GUARD_MODE: block
REQUIRE_API_KEY: "false"
run: |
node dist/server.js > server.log 2>&1 &
echo $! > server.pid
@@ -64,16 +65,20 @@ jobs:
# those 302s as "the API accepted a schema-violating request" and the configured-off
# 400 as "rejected a schema-compliant request". Documenting the flow in the spec is
# still right (operators need it); fuzzing it is not what this smoke is for.
# /api/auth/login has brute-force rate limiting: repeated failed logins return 429,
# which Schemathesis flags as rejection of schema-compliant requests.
schemathesis run docs/openapi.yaml --url http://localhost:20128 \
--include-path-regex '^/v1/(chat/completions|models)$|^/api/(auth|keys)' \
--exclude-path-regex '^/api/auth/oidc/' \
--exclude-path-regex '^/api/auth/(oidc/|login)' \
--max-examples 8 --workers 4 --checks all --max-response-time 30 \
--request-timeout 20 --suppress-health-check all --no-color
- name: Install promptfoo
run: npm install -g promptfoo@0.122.0
- name: promptfoo injection-guard (blocking)
env:
OMNIROUTE_URL: http://localhost:20128
OMNIROUTE_API_KEY: not-needed-blocked-before-upstream
run: npx --yes promptfoo@latest eval -c promptfooconfig.yaml --no-cache
run: promptfoo eval -c promptfooconfig.yaml --no-cache
- name: Stop server
if: always()
run: kill "$(cat server.pid)" || true

89
Dockerfile.bun Normal file
View File

@@ -0,0 +1,89 @@
# ── Multi-stage Dockerfile for Native Bun Runtime (web-latest-bun) ───────────
FROM oven/bun:1.3.14-slim AS base
WORKDIR /app
RUN apt-get update \
&& apt-get upgrade -y \
&& apt-get install -y --no-install-recommends \
build-essential \
python3 \
python-is-python3 \
make \
g++ \
libsecret-1-0 \
ca-certificates \
curl \
&& rm -rf /var/lib/apt/lists/*
# ── Builder stage (100% Bun Native Install & Build) ─────────────────────────
FROM base AS builder
WORKDIR /app
COPY . .
# Fast Bun native package install
RUN bun install --include=optional --quiet
# Compile native better-sqlite3 Node-API addon under Bun
RUN if [ -d "node_modules/better-sqlite3" ]; then \
(cd node_modules/better-sqlite3 && bunx node-gyp rebuild); \
fi
# Fetch tls-client-node native binary if script exists
RUN if [ -f "node_modules/tls-client-node/scripts/postinstall.js" ]; then \
bun node_modules/tls-client-node/scripts/postinstall.js || true; \
fi
# Disable Turbopack for Bun builder stage (Turbopack V8 internal worker bindings require Node)
ENV OMNIROUTE_USE_TURBOPACK=0
ARG OMNIROUTE_BASE_PATH=""
ENV OMNIROUTE_BASE_PATH=$OMNIROUTE_BASE_PATH
ARG DASHBOARD_ALLOW_EMBED=""
ENV DASHBOARD_ALLOW_EMBED=$DASHBOARD_ALLOW_EMBED
ENV NEXT_TELEMETRY_DISABLED=1
ENV NODE_ENV=production
# Bun native Next.js build execution
RUN bun run --quiet build
# ── Runner stage (100% Bun Native Production Runtime) ──────────────────────
FROM oven/bun:1.3.14-slim AS runner
LABEL org.opencontainers.image.title="omniroute" \
org.opencontainers.image.description="Unified AI proxy — route any LLM through one endpoint (Bun Native)" \
org.opencontainers.image.url="https://omniroute.online" \
org.opencontainers.image.source="https://github.com/diegosouzapw/OmniRoute" \
org.opencontainers.image.licenses="MIT"
WORKDIR /app
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
libsecret-1-0 \
ca-certificates \
curl \
&& rm -rf /var/lib/apt/lists/*
ENV NODE_ENV=production
ENV PORT=20128
ENV HOSTNAME=0.0.0.0
ENV OMNIROUTE_MEMORY_MB=1024
ENV DATA_DIR=/app/data
RUN mkdir -p /app/data
COPY --from=builder /app/.build/next/standalone ./
COPY --from=builder /app/node_modules/better-sqlite3 ./node_modules/better-sqlite3
ENV OMNIROUTE_MIGRATIONS_DIR=/app/migrations
COPY --from=builder /app/scripts/dev/healthcheck.mjs ./healthcheck.mjs
EXPOSE 20128
HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \
CMD bun healthcheck.mjs || exit 1
ENTRYPOINT ["bun", "bin/omniroute.mjs", "serve", "--no-open"]

View File

@@ -1009,6 +1009,19 @@ Full table: [Docker Guide — runtime RAM](docs/guides/DOCKER_GUIDE.md#runtime-r
> are **not supported for production**. See
> [Docker Release Channels](docs/guides/DOCKER_GUIDE.md#release-channels).
**🥟 Bun**
Standard `bun install` and global installation (`bun install -g omniroute`) are supported via Bun runtime detection:
- **Built-in `bun:sqlite`**: OmniRoute uses Bun's built-in `bun:sqlite` driver when running under Bun, falling back to `better-sqlite3` on Node.js or `sql.js`.
- **Automatic Webpack bundler selection**: Development (`bun run dev`) and production builds (`bun run build`) automatically detect Bun and disable Turbopack in favor of Webpack to prevent native V8 binding incompatibilities.
- **Dedicated Bun Dockerfile**: Multi-stage `Dockerfile.bun` for native Bun production deployments (`docker build -f Dockerfile.bun -t omniroute:bun .`).
```bash
# Install and run with Bun
bun install
bun run dev
```
**🛠️ From source**
```bash

View File

@@ -9,10 +9,13 @@ import { discoverPlugins } from "../plugins.mjs";
// (instead of string-interpolating into `execSync`) prevents a malicious plugin
// name like `foo; rm -rf ~` or `` foo`id` `` from being interpreted by the shell.
function runNpm(args) {
const res = spawnSync("npm", args, { stdio: "inherit", shell: false });
const isBun = Boolean(process.versions.bun);
const pm = isBun ? "bun" : "npm";
const cmdArgs = isBun && args[0] === "install" ? ["add", ...args.slice(1)] : args;
const res = spawnSync(pm, cmdArgs, { stdio: "inherit", shell: false });
if (res.error) throw res.error;
if (typeof res.status === "number" && res.status !== 0) {
throw new Error(`npm exited with code ${res.status}`);
throw new Error(`${pm} exited with code ${res.status}`);
}
}

View File

@@ -114,30 +114,30 @@ export function isBetterSqliteBinaryValid() {
export function npmInstallRuntime(pkgs, opts = {}) {
const cwd = ensureRuntimeDir();
// Persist to the runtime package.json (exact version) instead of --no-save so a later
// install of a sibling runtime dep (e.g. systray2 from trayRuntime.ts, which writes to the
// same runtime dir) does not prune this package as "extraneous" — that pruning otherwise
// reproduces "No SQLite driver available" after a tray install removes better-sqlite3.
// npm 12+ defaults `allowScripts` to off, silently skipping lifecycle/install
// scripts (e.g. better-sqlite3's node-gyp/prebuild-install rebuild) unless the
// package has a matching `allowScripts` entry — and still exits 0, masking the
// failure (#10713). The runtime dir is a CLI-owned, non-user package.json, so
// explicitly allowing scripts for the packages we are installing here is safe.
const npmArgs = [
"install",
...pkgs,
"--no-audit",
"--no-fund",
"--prefer-online",
"--save-exact",
...pkgs.map((pkg) => `--allow-scripts=${pkg}`),
];
// On Windows .cmd files cannot be executed without a shell; use cmd.exe /c explicitly
// so we never set shell:true (which would propagate env and enable injection).
const isWin = platform() === "win32";
const [exe, args] = isWin ? ["cmd.exe", ["/c", "npm", ...npmArgs]] : ["npm", npmArgs];
const isBun = Boolean(process.versions.bun);
let exe, args, displayCmd;
if (isBun) {
const bunArgs = ["add", ...pkgs, "--trust"];
[exe, args] = isWin ? ["cmd.exe", ["/c", "bun", ...bunArgs]] : ["bun", bunArgs];
displayCmd = `bun ${bunArgs.join(" ")}`;
} else {
const npmArgs = [
"install",
...pkgs,
"--no-audit",
"--no-fund",
"--prefer-online",
"--save-exact",
...pkgs.map((pkg) => `--allow-scripts=${pkg}`),
];
[exe, args] = isWin ? ["cmd.exe", ["/c", "npm", ...npmArgs]] : ["npm", npmArgs];
displayCmd = `npm ${npmArgs.join(" ")}`;
}
if (!opts.silent) {
process.stdout.write(`[omniroute][runtime] npm ${npmArgs.join(" ")}\n`);
process.stdout.write(`[omniroute][runtime] ${displayCmd}\n`);
}
const res = spawnSync(exe, args, {
cwd,

View File

@@ -5,10 +5,14 @@ import { ensureSettingsSchema, hashManagementPassword, updateSettings } from "./
async function loadSqlite() {
if (process.versions.bun) {
return { Database: (await import("bun:sqlite")).Database };
try {
return { Database: (await import("bun:sqlite")).Database, driver: "bun:sqlite" };
} catch (bunError) {
// fall through to better-sqlite3 if bun:sqlite fails
}
}
try {
return { Database: (await import("better-sqlite3")).default };
return { Database: (await import("better-sqlite3")).default, driver: "better-sqlite3" };
} catch (error) {
return { error };
}
@@ -86,12 +90,14 @@ export function normalizeBunSqliteParams(params) {
export function createSqliteNativeError(error) {
const message = error instanceof Error ? error.message : String(error);
const isBun = Boolean(process.versions.bun);
const rebuildCmd = isBun ? "bun add better-sqlite3 --trust" : "npm rebuild better-sqlite3";
if (message.includes("NODE_MODULE_VERSION") || message.includes("ERR_DLOPEN_FAILED")) {
return new Error(
"better-sqlite3 native binding is incompatible with this Node.js runtime. " +
"Run `npm rebuild better-sqlite3` in the OmniRoute project and try again. " +
"Or run: omniroute runtime repair " +
"(rebuilds into a user-writable runtime; works without a C++ toolchain)."
`better-sqlite3 native binding is incompatible with this runtime. ` +
`Run \`${rebuildCmd}\` in the OmniRoute project and try again. ` +
`Or run: omniroute runtime repair ` +
`(rebuilds into a user-writable runtime; works without a C++ toolchain).`
);
}
if (
@@ -100,10 +106,9 @@ export function createSqliteNativeError(error) {
message.includes("Cannot find module 'better-sqlite3'")
) {
return new Error(
"better-sqlite3 native binding could not be found (no prebuilt addon for this platform). " +
"This is common under `npx`, which runs a fresh, ephemeral install that never built the addon. " +
"Run: omniroute runtime repair " +
"(rebuilds into a user-writable runtime; works without a C++ toolchain)."
`better-sqlite3 native binding could not be found (no prebuilt addon for this platform). ` +
`Run: omniroute runtime repair ` +
`(rebuilds into a user-writable runtime; works without a C++ toolchain).`
);
}
return error;
@@ -111,7 +116,7 @@ export function createSqliteNativeError(error) {
async function openSqliteDatabase(dbPath, options = {}) {
const loaded = await loadSqlite();
if (process.versions.bun) {
if (loaded.driver === "bun:sqlite" || (process.versions.bun && !loaded.Database)) {
if (options.fileMustExist && !fs.existsSync(dbPath)) {
throw new Error(`SQLite file does not exist: ${dbPath}`);
}

View File

@@ -94,10 +94,15 @@ export function ensureAndroidCacheDir(options = {}) {
*/
export function isFatalInstrumentationHookFailure(text) {
if (!text) return false;
return (
/Unsupported platform:\s*android/i.test(text) ||
/error occurred while loading instrumentation hook/i.test(text)
);
// Next.js wraps ANY throw inside instrumentation.register() with the generic
// "An error occurred while loading instrumentation hook:" prefix, on every
// platform (node_modules/next/dist/server/web/globals.js). That prefix alone
// therefore cannot identify the Android/Termux cache-probe failure — a bare
// generic instrumentation error on win32/desktop would be misreported as the
// Android bug and hide the real cause. Only match when the text actually
// carries the Android platform marker that Next's getCacheDirectory() emits.
// #10028
return /Unsupported platform:\s*android/i.test(text);
}
/**

View File

@@ -44,6 +44,18 @@ export function getSecureFloorForMajor(major) {
}
export function getNodeRuntimeSupport(version = process.versions.node) {
if (process.versions.bun) {
return {
nodeVersion: `bun-${process.versions.bun} (Node.js API ${version})`,
nodeCompatible: true,
reason: "supported-bun",
supportedRange: SUPPORTED_NODE_RANGE + " || Bun >=1.1.0",
supportedDisplay: SUPPORTED_NODE_DISPLAY + ", or Bun 1.1+",
recommendedVersion: `v${RECOMMENDED_NODE_VERSION}`,
minimumSecureVersion: null,
};
}
const parsed = parseNodeVersion(version);
const secureFloor = getSecureFloorForMajor(parsed.major);
const nodeCompatible = secureFloor ? compareNodeVersions(parsed, secureFloor) >= 0 : false;

View File

@@ -17,7 +17,12 @@
import { existsSync, readFileSync, writeFileSync } from "node:fs";
import { join, dirname } from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import updateNotifier from "update-notifier";
let updateNotifier = null;
try {
updateNotifier = (await import("update-notifier")).default;
} catch {
// update-notifier is optional in pruned standalone environments
}
import { isNativeBinaryCompatible } from "../scripts/build/native-binary-compat.mjs";
import { getNodeRuntimeSupport, getNodeRuntimeWarning } from "./nodeRuntimeSupport.mjs";
import { getDefaultDataDir } from "./cli/data-dir.mjs";
@@ -251,8 +256,9 @@ if (shouldProvisionStorageKey(process.argv)) {
// Register update notifier — checks npm once per 24h, notifies on exit via stderr.
const _pkg = JSON.parse(readFileSync(join(ROOT, "package.json"), "utf8"));
const _notifier = updateNotifier({ pkg: _pkg, updateCheckInterval: 1000 * 60 * 60 * 24 });
const _notifier = updateNotifier ? updateNotifier({ pkg: _pkg, updateCheckInterval: 1000 * 60 * 60 * 24 }) : null;
process.on("exit", () => {
if (!_notifier || !_notifier.update) return;
if (process.env.OMNIROUTE_NO_UPDATE_NOTIFIER) return;
if (process.env.CI) return;
if (process.argv.includes("--quiet") || process.argv.includes("-q")) return;

View File

@@ -0,0 +1 @@
- fix(cli): stop diagnosing every Next.js instrumentation-hook failure as the Android/Termux cache bug — only the Android "Unsupported platform: android" signal now triggers the Android hint, so a win32/desktop instrumentation error surfaces its real cause instead of a useless `mkdir -p ~/.cache` (#10028)

View File

@@ -0,0 +1 @@
- fix(command-code): route chat to the documented /provider/v1/chat/completions endpoint instead of the CLI-only /alpha/generate, which Command Code gates/blocks for external callers (#10265)

View File

@@ -0,0 +1 @@
- fix(services): isolate probeBeforeSpawn adoption tests on distinct ports to stop the order-dependent flake (#10523)

View File

@@ -0,0 +1 @@
- fix(command-code): surface reasoning-only output as content when a model emits no text-delta (#10986)

View File

@@ -0,0 +1 @@
- **Static model catalog for v0-vercel-web:** seed a static catalog for the v0-vercel-web web-cookie provider (v0-1.0-md, v0-1.5-lg, v0-1.5-md) so its dashboard "Available Models" / "Import from /models" UI serves a usable list instead of falling through to the route's 400 "does not support models listing" ([#10990](https://github.com/diegosouzapw/OmniRoute/issues/10990)).

View File

@@ -0,0 +1 @@
- fix(providers): mark the blackbox provider deprecated — api.blackbox.ai returns HTTP 404 on every path variant (sweep 2026-08-21), so the public inference surface is dead and the catalog entry now carries a deprecation notice. ([#10997](https://github.com/diegosouzapw/OmniRoute/issues/10997))

View File

@@ -0,0 +1 @@
- fix(providers): validate Dify keys against its native /v1/chat-messages endpoint (#11002)

View File

@@ -0,0 +1 @@
- **fix(webhooks):** remove 3 declared-but-never-emitted events (`provider.error`, `provider.recovered`, `combo.switched`) from `WebhookEvent` — catalog now `request.completed | request.failed | quota.exceeded | test.ping`; `POST /api/webhooks` and `PUT /api/webhooks/[id]` reject ghost values with 400; OpenAPI webhook description updated across 43 locales ([11050](https://github.com/diegosouzapw/OmniRoute/pull/11050))

View File

@@ -0,0 +1 @@
- fix(dashboard): treat UncloseAI as a no-auth provider so the connect form no longer forces a fake API key (#8864)

View File

@@ -0,0 +1 @@
- fix(ssrf): make `getProviderOutboundGuard()` (used for search-provider connection validation, image generation and remote image fetch) honor the local-first default `OMNIROUTE_ALLOW_LOCAL_PROVIDER_URLS` the same way the chat validation guard already does, so a LAN-hosted SearXNG/Brave search provider works with only the LOCAL flag set instead of silently requiring `OMNIROUTE_ALLOW_PRIVATE_PROVIDER_URLS` ([#9123](https://github.com/diegosouzapw/OmniRoute/issues/9123)).

View File

@@ -0,0 +1,3 @@
- fix(api): repair broken `@/lib/db/connections` import in the usage utilization route that failed the production build (#10939 follow-up)
- chore(docs): regenerate PROVIDER_REFERENCE and refresh README diagram SVGs to the real provider count (347)
- chore(lint): prune ESLint suppressions orphaned on the release branch

View File

@@ -0,0 +1 @@
- **test(db):** replace three empty `test.skip` placeholders in the critical DB-state suite with real assertions — `resetDbInstance` must swap the singleton while the on-disk row survives, the on-disk DB must open in WAL journal mode, and `db_meta` must hold the seeded `schema_version` — so a regression in any of those invariants can no longer pass as silently green ([#10906](https://github.com/diegosouzapw/OmniRoute/pull/10906))

View File

@@ -0,0 +1 @@
- fix(quality): rebaseline file-size for modelCapabilities.ts (1016->1072) drift from merged tip fixes (#11034 et al)

View File

@@ -1,9 +1,6 @@
{
"open-sse/services/payloadRules.ts": {
"TS2677": 1
},
"src/app/(dashboard)/dashboard/HomePageClient.tsx": {
"TS2339": 16
"TS2339": 10
},
"src/app/(dashboard)/dashboard/agent-skills/AgentSkillsPageClient.tsx": {
"TS2503": 3
@@ -120,10 +117,6 @@
"src/app/(dashboard)/dashboard/providers/[id]/components/CompatibleModelsSection.tsx": {
"TS2741": 1
},
"src/app/(dashboard)/dashboard/providers/[id]/components/ConnectionRow.tsx": {
"TS2345": 3,
"TS2322": 1
},
"src/app/(dashboard)/dashboard/providers/[id]/components/ConnectionsListPanel.tsx": {
"TS2322": 2
},
@@ -141,12 +134,6 @@
"src/app/(dashboard)/dashboard/providers/[id]/components/ProviderPlaygroundPanel.tsx": {
"TS2503": 1
},
"src/app/(dashboard)/dashboard/providers/[id]/components/modals/EditConnectionModal.tsx": {
"TS2322": 1
},
"src/app/(dashboard)/dashboard/providers/[id]/hooks/useModelImportHandlers.ts": {
"TS2339": 1
},
"src/app/(dashboard)/dashboard/providers/[id]/hooks/useModelVisibilityHandlers.ts": {
"TS2339": 15
},
@@ -190,9 +177,6 @@
"src/lib/combos/builderDraft.ts": {
"TS2741": 1
},
"src/lib/providers/codexFastTier.ts": {
"TS2367": 1
},
"src/lib/services/htmlRewriter.ts": {
"TS2322": 2,
"TS2345": 2
@@ -219,14 +203,7 @@
"src/shared/hooks/useElectron.ts": {
"TS2339": 19
},
"src/shared/providers/webSessionCredentials.ts": {
"TS2353": 1,
"TS2322": 1
},
"src/shared/schemas/cliCatalog.ts": {
"TS2554": 2
},
"src/shared/services/opencodeConfig.ts": {
"TS2345": 1
}
}

View File

@@ -443,21 +443,24 @@
"src/shared/components/ModelSelectModal.tsx": 1138,
"src/shared/constants/providers/apikey/gateways.ts": 1250
},
"src/app/(dashboard)/dashboard/providers/[id]/components/modals/AddApiKeyModal.tsx": 1067,
"src/app/(dashboard)/dashboard/providers/[id]/components/modals/AddApiKeyModal.tsx": 1080,
"src/app/(dashboard)/dashboard/providers/[id]/hooks/useProviderConnections.ts": 1051,
"src/shared/components/ModelSelectModal.tsx": 1138,
"src/shared/constants/providers/apikey/gateways.ts": 1298,
"open-sse/vendor/codex-chatgpt-web/bridge.ts": 1387,
"_rebaseline_2026_08_11_v3850_merge_storm_provider_registry": "DRIFT do merge-storm 2026-08-11 (99 PRs mergeados no release/v3.8.50). AddApiKeyModal.tsx (PR #8949 ChatGPT Web provider) e useProviderConnections.ts/ModelSelectModal.tsx (PRs #9011 combo test-all, #9499 image combos) = UI nova legitima acima do cap; gateways.ts = god-file de catalogo de providers que cresceu com PRs #9009/#9421/#9468/#9594 (qualquer split arriscaria corromper o merge de novo — o proprio PR #9421 quebrou o arquivo); bridge.ts (PR #8949) = ponte Chromium vendored; proxyFetch.ts 1207->1220 = drift herdado de merges. Owner autorizou rebaseline com anotacao (2026-08-11).",
"src/lib/modelCapabilities.ts": 1016,
"src/lib/modelCapabilities.ts": 1072,
"_rebaseline_2026_08_21_11034_effort_variants": "DRIFT do tip (base-red #9985): modelCapabilities.ts 1016->1072 (+56) acumulado por PRs ja mergeadas no release/v3.8.50 — principalmente #11034 (resolve effort-variant capabilities a partir do modelo base), alem de #10963/#11040/#10987 growth dos catalogos. Tip puro ficou vermelho neste gate; rebaseline no tip por push direto (owner pre-autorizou crescimento legitimo). Nao tocou no arquivo da #11038.",
"src/app/(dashboard)/dashboard/providers/[id]/providerPageHelpers.ts": 1014,
"open-sse/config/imageRegistry.ts": 1034,
"src/sse/handlers/chatHelpers.ts": 1019,
"src/shared/middleware/chatBodyAdmission.ts": 1005,
"_rebaseline_2026_08_20_10668_tabitoken_gateway": "#10668 (yawar-aquil) own catalog growth: src/shared/constants/providers/apikey/gateways.ts 1268->1283 (+15, entirely this PR diff -- one new tabitoken gateway entry, data lines only; base moved from 1255 to 1268 via other merges since the PR forked). Not combination drift: reproducible on the PR branch alone, so the WS5.5 release-captain rule does not apply. Extraction is not available -- the file is pure data (own header: \"Pure data; merged by apikey/index.ts via spread\") and already split into 6 family files under apikey/. Same precedent as _rebaseline_2026_08_14_imagetotext_servicekinds (#10275/#10291, gateways.ts 1250->1255, data lines only) and _rebaseline_2026_08_11_v3850_merge_storm_provider_registry (owner-authorized for this same file).",
"open-sse/executors/commandCode.ts": 1038,
"open-sse/executors/commandCode.ts": 1059,
"_rebaseline_2026_08_21_10859_vision_bridge_catalog": "#10859 own growth (Vision Bridge fixes #10808/#10809): src/lib/modelCapabilities.ts 1006->1016 (+10, cmd/gpt-5.3-codex* text-only capability resolution) and open-sse/executors/commandCode.ts 988->1023 (+35, Command Code wire-model normalization for bare ids + reasoning field fallback for opencode-routed gateways). Cohesive bug fixes at the existing capability-resolution / executor chokepoints; not extractable mid-fix. Covered by tests/unit/model-capabilities-command-code-codex-textonly-10703.test.ts, tests/unit/command-code-vision.test.ts, tests/unit/opencode-mimo-reasoning-details-nonstream.test.ts. Pushed directly to release (own-session miss: the original rebaseline was made in a throwaway validation worktree and never landed on the PR branch or the release before merge).",
"_rebaseline_2026_08_21_10907_sticky_pin_clear": "#10907 own growth: open-sse/executors/commandCode.ts 1023->1038 (+15, effort-suffix sanitization threading for the sticky-pin-clear fix). Cohesive change at the existing executor chokepoint. Covered by tests/unit/command-code-executor.test.ts."
"_rebaseline_2026_08_21_10907_sticky_pin_clear": "#10907 own growth: open-sse/executors/commandCode.ts 1023->1038 (+15, effort-suffix sanitization threading for the sticky-pin-clear fix). Cohesive change at the existing executor chokepoint. Covered by tests/unit/command-code-executor.test.ts.",
"_rebaseline_2026_08_21_10986_reasoning_only_content": "#10986 own growth: open-sse/executors/commandCode.ts 1038->1059 (+21, reasoning-only content fallback — when upstream emits only reasoning-delta events and never a text-delta, surface the reasoning text as message.content in createJsonResponse and emit a synthetic content delta in createStreamResponse). Cohesive bug fix at the existing executor chokepoint (mirrors precedent style of #10907/#10859). Covered by tests/unit/command-code-executor.test.ts (2 new cases: non-stream + streaming).",
"_rebaseline_2026_08_21_11069_m365_har_import": "#11069 own growth: AddApiKeyModal.tsx 1073->1080 (+7 = Import .har file button for the copilot-m365-web credential modal — M365 is the only provider whose credential (access_token+chathubPath) must be extracted from a DevTools HAR WebSocket URL, added as a new modal affordance). Cohesive UI at the existing modal chokepoint; not extractable. Covered by tests/unit/m365-har-import*.test.ts."
},
"_rebaseline_base_2026_08_10_proxyfetch": "Base-red fix (green-prs sweep, issue #9985): open-sse/utils/proxyFetch.ts 1207 > cap 1000 — new proxied-TLS fetch helper introduced by the Fal reference-image work. Owner-authorized quick rebaseline to green; structural slim tracked for v3.9.0.",
"_rebaseline_2026_07_27_v3849_train2": "Merge-train 2 (7 PRs) — owner-approved 2026-07-27. Single entry: chatCore.ts 4955->5006 (#8595, Responses multi-turn image compaction before the context hard-reject). Genuine irreducible growth at the existing compaction chokepoint in handleChatCore — the PR adds a last-resort retry against the concrete budget plus the estimateFinalInputTokens helper, both wired at the pre-existing call site rather than a new branch. Covered by tests/unit/8560-responses-image-compaction.test.ts (4 tests).",

View File

@@ -1,11 +1,4 @@
{
"open-sse/handlers/chatCore/clientUsageBuffer.ts": {
"TS2345": 2
},
"open-sse/utils/stream.ts": {
"TS2345": 2,
"TS2322": 2
},
"src/lib/guardrails/videoBridgeHelpers.ts": {
"TS2488": 1,
"TS2365": 2,

View File

@@ -0,0 +1,16 @@
/**
* Models declared identically in both the `opencode-zen` and `opencode-go` provider
* registries (same upstream family, opencode.ai/zen/*). Mirrors the GLM_SHARED_MODELS
* pattern in glmProvider.ts: one array, spread into each sibling RegistryEntry, so a
* metadata fix (targetFormat, supportsReasoning, ...) only has to land in one file
* instead of drifting out of sync across registries.
*
* Only entries that are byte-identical across both registries belong here — a model
* with tier-specific flags (e.g. go's effort variants, or a flag only one tier needs)
* stays local to that registry's own `models` array.
*/
export const OPENCODE_ZEN_GO_SHARED_MODELS = Object.freeze([
{ id: "kimi-k2.7-code", name: "Kimi K2.7 Code" },
{ id: "qwen3.5-plus", name: "Qwen3.5 Plus", targetFormat: "claude", supportsVision: false },
{ id: "qwen3.6-plus", name: "Qwen3.6 Plus", targetFormat: "claude", supportsVision: false },
]);

View File

@@ -5,6 +5,12 @@ export const blackboxProvider: RegistryEntry = {
alias: "bb",
format: "openai",
executor: "default",
// NOTE: api.blackbox.ai returns HTTP 404 on /v1/chat/completions and /v1/models
// (empty body, all path variants) since sweep 2026-08-21; the public inference
// surface has moved to the gated enterprise.blackbox.ai/v1 endpoint. The provider
// is marked deprecated in src/shared/constants/providers/apikey/frontier-labs.ts —
// this registry entry is kept intact (registration/execution unaffected), so
// existing configured keys keep working if a restored/enterprise host is reachable.
baseUrl: "https://api.blackbox.ai/v1/chat/completions",
modelsUrl: "https://api.blackbox.ai/v1/models",
authType: "apikey",

View File

@@ -8,7 +8,11 @@ export const command_codeProvider: RegistryEntry = {
format: "openai",
executor: "command-code",
baseUrl: "https://api.commandcode.ai",
chatPath: "/alpha/generate",
// Chat uses the documented /provider/v1/chat/completions (OpenAI-format)
// endpoint — NOT the CLI-only /alpha/generate endpoint, which Command Code
// version-gates and proxy-blocks for external callers (#10265). Discovery
// already targets the sibling /provider/v1/models endpoint.
chatPath: "/provider/v1/chat/completions",
modelsUrl: "https://api.commandcode.ai/provider/v1/models",
// The discovery response is a partial routing catalog; static registry
// entries omitted from it can still be accepted by the gateway.

View File

@@ -5,7 +5,11 @@ export const difyProvider: RegistryEntry = {
alias: "dify",
format: "openai",
executor: "default",
baseUrl: "https://api.dify.ai/v1/chat/completions",
// Dify does not serve /chat/completions — its native completion route is
// POST /v1/chat-messages (validated via the dedicated dify validator, #11002).
// Keep this as the bare API root so route suffixes build correctly and
// self-hosted instances can override the base URL per connection.
baseUrl: "https://api.dify.ai",
authType: "apikey",
authHeader: "bearer",
models: [{ id: "auto", name: "Auto" }],

View File

@@ -16,7 +16,7 @@ export const hailuo_webProvider: RegistryEntry = {
alias: "hailuo-web",
format: "openai",
executor: "hailuo-web",
baseUrl: "https://www.hailuo.ai",
baseUrl: "https://chat.minimax.io",
authType: "apikey",
authHeader: "bearer",
models: HAILUO_WEB_STATIC_MODELS,

View File

@@ -1,4 +1,5 @@
import type { RegistryEntry } from "../../../shared.ts";
import { OPENCODE_ZEN_GO_SHARED_MODELS } from "../../../shared.ts";
export const opencode_goProvider: RegistryEntry = {
id: "opencode-go",
@@ -13,6 +14,8 @@ export const opencode_goProvider: RegistryEntry = {
authPrefix: "Bearer",
defaultContextLength: 200000,
models: [
...OPENCODE_ZEN_GO_SHARED_MODELS,
// Port from decolua/9router 8efacc11: align with official Go endpoints —
// glm-5.2 is now advertised and Kimi chat traffic must route through
// `kimi-k2.7-code` (the live API rejects the plain `kimi-k2.7` alias for
@@ -25,7 +28,7 @@ export const opencode_goProvider: RegistryEntry = {
{ id: "glm-5.2-max", name: "GLM-5.2 (max effort)", supportsReasoning: true },
{ id: "glm-5.1", name: "GLM-5.1" },
{ id: "glm-5", name: "GLM-5" },
{ id: "kimi-k2.7-code", name: "Kimi K2.7 Code" },
// kimi-k2.7-code declared identically on opencode-zen — see OPENCODE_ZEN_GO_SHARED_MODELS.
{ id: "kimi-k2.6", name: "Kimi K2.6" },
{ id: "kimi-k2.5", name: "Kimi K2.5" },
// #8353: Kimi K3 base + max-effort alias from the OpenCode Go registry.
@@ -89,7 +92,8 @@ export const opencode_goProvider: RegistryEntry = {
supportsVision: false,
supportsReasoning: true,
},
{ id: "qwen3.6-plus", name: "Qwen3.6 Plus", targetFormat: "claude", supportsVision: false },
// qwen3.6-plus / qwen3.5-plus base ids declared identically on opencode-zen — see
// OPENCODE_ZEN_GO_SHARED_MODELS.
{
id: "qwen3.6-plus-high",
name: "Qwen3.6 Plus (high effort)",
@@ -104,7 +108,6 @@ export const opencode_goProvider: RegistryEntry = {
supportsVision: false,
supportsReasoning: true,
},
{ id: "qwen3.5-plus", name: "Qwen3.5 Plus", targetFormat: "claude", supportsVision: false },
// #8353: hy3 is the Go-tier base id (distinct from hy3-preview / hy3-free).
{ id: "hy3", name: "Hunyuan3", contextLength: 256000, supportsReasoning: true },
{
@@ -138,6 +141,7 @@ export const opencode_goProvider: RegistryEntry = {
supportsVision: true,
supportsAudio: true,
supportsVideo: true,
targetFormat: "openai-responses",
},
{
id: "muse-spark-1.2-contributor-minimal",
@@ -148,6 +152,7 @@ export const opencode_goProvider: RegistryEntry = {
supportsVision: true,
supportsAudio: true,
supportsVideo: true,
targetFormat: "openai-responses",
},
{
id: "muse-spark-1.2-contributor-low",
@@ -158,6 +163,7 @@ export const opencode_goProvider: RegistryEntry = {
supportsVision: true,
supportsAudio: true,
supportsVideo: true,
targetFormat: "openai-responses",
},
{
id: "muse-spark-1.2-contributor-medium",
@@ -168,6 +174,7 @@ export const opencode_goProvider: RegistryEntry = {
supportsVision: true,
supportsAudio: true,
supportsVideo: true,
targetFormat: "openai-responses",
},
{
id: "muse-spark-1.2-contributor-high",
@@ -178,6 +185,7 @@ export const opencode_goProvider: RegistryEntry = {
supportsVision: true,
supportsAudio: true,
supportsVideo: true,
targetFormat: "openai-responses",
},
{
id: "muse-spark-1.2-contributor-xhigh",
@@ -188,6 +196,7 @@ export const opencode_goProvider: RegistryEntry = {
supportsVision: true,
supportsAudio: true,
supportsVideo: true,
targetFormat: "openai-responses",
},
// #8353: Grok 4.5 + effort tiers from the OpenCode Go registry.
{ id: "grok-4.5", name: "Grok 4.5", supportsReasoning: true },

View File

@@ -1,4 +1,5 @@
import type { RegistryEntry } from "../../../shared.ts";
import { OPENCODE_ZEN_GO_SHARED_MODELS } from "../../../shared.ts";
export const opencode_zenProvider: RegistryEntry = {
id: "opencode-zen",
@@ -15,6 +16,8 @@ export const opencode_zenProvider: RegistryEntry = {
// from the live API response so new models work without a code deploy.
passthroughModels: true,
models: [
...OPENCODE_ZEN_GO_SHARED_MODELS,
// ── Chat / Coding ──────────────────────────────────────────
// #2900: big-pickle's upstream runs DeepSeek thinking mode — declare the
// interleaved reasoning_content contract so follow-up/tool-use turns replay
@@ -51,7 +54,25 @@ export const opencode_zenProvider: RegistryEntry = {
{ id: "grok-4.6", name: "Grok 4.6" },
// ── Muse ───────────────────────────────────────────────────
{ id: "muse-spark-1.2", name: "Muse Spark 1.2" },
// Muse Spark is served by OpenCode Zen only on the OpenAI Responses API
// endpoint, not /chat/completions (see the opencode provider's own
// muse-spark entries, #10874/#10867) — this provider is a separate
// registry entry for the same upstream and never got the same
// targetFormat declaration, so requests routed here still hit
// /chat/completions with a mismatched or unanswerable body and the
// upstream returns an empty message.
{
id: "muse-spark-1.2",
name: "Muse Spark 1.2",
supportsReasoning: true,
targetFormat: "openai-responses",
},
{
id: "muse-spark-1.2-contributor-free",
name: "Muse Spark 1.2 Contributor Free",
supportsReasoning: true,
targetFormat: "openai-responses",
},
// ── DeepSeek ────────────────────────────────────────────────
{ id: "deepseek-v4-pro", name: "DeepSeek V4 Pro" },
@@ -66,7 +87,7 @@ export const opencode_zenProvider: RegistryEntry = {
// ── Kimi / Moonshot ────────────────────────────────────────
{ id: "kimi-k3", name: "Kimi K3" },
{ id: "kimi-k2.7-code", name: "Kimi K2.7 Code" },
// kimi-k2.7-code declared identically on opencode-go — see OPENCODE_ZEN_GO_SHARED_MODELS.
// ── Qwen ───────────────────────────────────────────────────
// Issue #2292: Qwen models return Claude-format SSE bodies even
@@ -74,8 +95,8 @@ export const opencode_zenProvider: RegistryEntry = {
// through /messages and the Claude translator.
// Issue #2822: These models are text-only — supportsVision: false
// ensures combo routing skips them on image-bearing requests.
{ id: "qwen3.5-plus", name: "Qwen3.5 Plus", targetFormat: "claude", supportsVision: false },
{ id: "qwen3.6-plus", name: "Qwen3.6 Plus", targetFormat: "claude", supportsVision: false },
// qwen3.5-plus / qwen3.6-plus declared identically on opencode-go — see
// OPENCODE_ZEN_GO_SHARED_MODELS.
// ── Free Tier ──────────────────────────────────────────────
// #6998 (2026-07-14): upstream free tier rotated — minimax-m2.5-free,

View File

@@ -25,6 +25,7 @@ import {
GLMT_TIMEOUT_MS,
GLM_SHARED_MODELS,
} from "../glmProvider.ts";
import { OPENCODE_ZEN_GO_SHARED_MODELS } from "../opencodeZenGoSharedModels.ts";
import { MARITALK_DEFAULT_BASE_URL } from "../maritalk.ts";
import {
CURSOR_REGISTRY_VERSION,
@@ -719,6 +720,7 @@ export {
GLM_TIMEOUT_MS,
GLMT_TIMEOUT_MS,
GLM_SHARED_MODELS,
OPENCODE_ZEN_GO_SHARED_MODELS,
MARITALK_DEFAULT_BASE_URL,
CURSOR_REGISTRY_VERSION,
getAntigravityProviderHeaders,

View File

@@ -104,6 +104,12 @@ import {
import { applyPeerTraceHeader } from "@/shared/resilience/peerRouting";
import { applyClineProtocolHeaders } from "@/shared/utils/clineAuth";
import { isProbeContext } from "@/shared/utils/probeOrigin";
import {
parseAndValidatePublicUrl,
parseAndValidateNonMetadataUrl,
} from "@/shared/network/outboundUrlGuard";
import { getProviderValidationGuard } from "@/shared/network/outboundUrlGuardPolicy";
import { isLocalProvider, isSelfHostedChatProvider } from "@/shared/constants/providers";
// Header helpers extracted to a pure leaf; re-exported for external importers
// (executors + tests) that import them from "./base.ts".
export {
@@ -397,6 +403,29 @@ export class BaseExecutor {
return fallback || this.config.baseUrl || "";
}
/**
* SSRF guard for the runtime dispatch path (GHSA-4f49-hj64-448x). A persisted,
* caller-supplied `providerSpecificData.baseUrl` reaches the fetch() calls
* below, so a `manage`-scope actor (or, on a keyless install, an anonymous
* one) could point a provider at loopback / internal / cloud-metadata hosts
* and exfiltrate the stored upstream key. Mirror the provider VALIDATION
* guard so runtime dispatch makes the same decision the validation layer
* already makes: local / self-hosted providers are exempt (they legitimately
* use private URLs, and the OMNIROUTE_ALLOW_PRIVATE_PROVIDER_URLS opt-in still
* applies through the guard), and for everything else `public-only` mode
* blocks private + metadata while the default `block-metadata` mode blocks the
* cloud-metadata IMDS pivot. Throws on a blocked URL.
*/
protected assertOutboundUrlAllowed(url: string): void {
if (!url) return;
if (isLocalProvider(this.provider) || isSelfHostedChatProvider(this.provider)) return;
if (getProviderValidationGuard() === "public-only") {
parseAndValidatePublicUrl(url);
return;
}
parseAndValidateNonMetadataUrl(url);
}
/**
* Alternate protocol selected on this connection, if the provider declares one
* that matches. Centralizes the registry lookup so every call-site resolves the
@@ -615,6 +644,7 @@ export class BaseExecutor {
async countTokens({ model, body, credentials, signal, log }: CountTokensInput) {
const url = this.buildCountTokensUrl(model, credentials);
if (!url) return null;
this.assertOutboundUrlAllowed(url); // GHSA-4f49
const headers = this.buildHeaders(credentials, false);
const requestBody =
@@ -869,6 +899,9 @@ export class BaseExecutor {
// Timeout only covers response start; stream stalls are handled downstream.
const fetchStartTimeoutMs = this.getTimeoutMs();
const fetchWithStartTimeout = async (requestUrl: string, requestOptions: RequestInit) => {
// GHSA-4f49: guard here (not only next to the first buildUrl) so retries
// and fallback URLs are validated too, before any bytes leave the host.
this.assertOutboundUrlAllowed(requestUrl);
const timeoutController = fetchStartTimeoutMs > 0 ? new AbortController() : null;
let timeoutId: ReturnType<typeof setTimeout> | null = null;
if (timeoutController) {

File diff suppressed because it is too large Load Diff

View File

@@ -30,17 +30,45 @@ export const HANDSHAKE_REQUEST = { protocol: "json", version: 1 } as const;
export const KEEPALIVE_PING = { type: 6 } as const;
/**
* Allowed message types observed in the 2026-08 recapture of the working
* `m365.cloud.microsoft/chat` client (#10718). The old 11-entry list is no longer
* seen on the wire — the stale shape gets closed immediately after the type:4.
* Allowed message types observed in a 2026-08-21 live capture of a working
* `m365.cloud.microsoft/chat` session (issue: "Stream ended before producing a
* non-ping SSE event" on every individual/consumer M365 Copilot call). The
* #10718 6-entry shape above no longer produces a `type:1 target:"update"`
* frame at all — the socket only replies with SignalR keepalive pings and then
* closes, which is exactly what surfaces client-side as that generic stream
* error. 30 entries, up from 6.
*/
export const ALLOWED_MESSAGE_TYPES = [
"Chat",
"Suggestion",
"InternalSearchQuery",
"Disengaged",
"Progress",
"EndOfRequest",
"InternalLoaderMessage",
"Progress",
"GeneratedCode",
"RenderCardRequest",
"AdsQuery",
"SemanticSerp",
"GenerateContentQuery",
"GenerateGraphicArt",
"SearchQuery",
"ConfirmationCard",
"AuthError",
"DeveloperLogs",
"TriggerPlugin",
"HintInvocation",
"MemoryUpdate",
"EndOfRequest",
"TriggerConfirmation",
"ResumeInvokeAction",
"ResumeUserInputRequest",
"TriggerUserInputRequest",
"EscapeHatch",
"TriggerPluginAuth",
"ResumePluginAuth",
"SideBySide",
"ReferencesListComplete",
"SwitchRespondingEndpoint",
] as const;
/**
@@ -78,19 +106,26 @@ export const M365_ENTERPRISE_EXTRA_MESSAGE_TYPES = [
] as const;
/**
* Individual / EDU option sets from the 2026-08 recapture (#10718)14 entries.
* The previous 25-entry consumer/MSA set (enable_msa_user, pdnascan, cwc_code_*,
* …) is no longer observed on the wire and belongs to the shape the substrate
* now drops silently.
* Individual / EDU option sets from a 2026-08-21 live capture34 entries, up
* from the #10718 14-entry shape (which itself superseded an earlier 25-entry
* shape). Each recapture so far has been additive/reshuffled rather than a
* wholesale replacement — treat this as the protocol continuing to drift, not
* a one-time fix; a future capture may again need to update this list.
*/
export const M365_DEFAULT_OPTION_SETS = [
"search_result_progress_messages_with_search_queries",
"update_textdoc_response_after_streaming",
"deepleo_networking_timeout_10minutes_canmore",
"cwc_flux_image",
"cwc_code_interpreter",
"cwc_code_interpreter_amsfix",
"cwcfluxgptv",
"flux_v3_gptv_enable_upload_multi_image_in_turn_wo_ch",
"gptvnorm2048",
"cwc_code_interpreter_citation_fix",
"code_interpreter_interactive_charts",
"cwc_code_interpreter_interactive_charts_inline_image",
"code_interpreter_matplotlib_patching",
"cwc_fileupload_odb",
"update_memory_plugin",
"add_custom_instructions",
@@ -98,6 +133,20 @@ export const M365_DEFAULT_OPTION_SETS = [
"flux_v3_progress_messages",
"enable_batch_token_processing",
"enable_gg_gpt",
"async_client_interaction",
"flux_v3_references",
"flux_v3_references_entities",
"flux_v3_references_ci",
"add_filestore_filetype",
"cwc_code_interpreter_citation_sourceannotations",
"cdxcwc_code_interpreter_hallucinated_url_filter",
"flux_v3_image_gen_enable_dimensions",
"flux_v3_image_gen_enable_non_watermarked_storage",
"flux_v3_image_gen_enable_icon_dimensions",
"flux_v3_image_gen_enable_system_text_with_params",
"flux_v3_image_gen_enable_designer_dimensions_meta_prompting_in_system_prompts",
"flux_v3_image_gen_enable_story",
"rich_responses",
] as const;
/** Append the record separator to a JSON-serializable frame. */
@@ -433,12 +482,14 @@ export function resolveChatInvocationOverrides(tier: string | undefined): {
}
return {
optionsSets: [...M365_DEFAULT_OPTION_SETS],
// #10718 — the 2026-08 recapture sends tone:"magic" (lowercase) on the
// individual/EDU surface; the old "" default is part of the dropped shape.
tone: "magic",
// 2026-08-21 capture — the individual/consumer surface now sends "Magic"
// (capitalized), matching the enterprise tone literal. The #10718
// lowercase "magic" is part of the shape that gets silently dropped.
tone: "Magic",
allowedMessageTypes: ALLOWED_MESSAGE_TYPES,
// Omitted entirely on the individual/EDU wire (see ChatInvocationOptions).
disconnectBehavior: undefined,
// 2026-08-21 capture — disconnectBehavior:"continue" is now present on the
// individual/consumer wire too, not just enterprise (see ChatInvocationOptions).
disconnectBehavior: "continue",
};
}
@@ -467,16 +518,33 @@ export function resolveToneForModel(model: string | undefined): string | undefin
/**
* Build the `type:4` chat invocation frame body (not yet `\x1e`-terminated).
* Mirrors the argument shape recaptured from a working `m365.cloud.microsoft/chat`
* client in 2026-08 (#10718). Notable differences from the pre-#10718 shape: a
* populated `clientInfo` + `productThreadType:"Office"`, a `conversationId`
* matching the WS URL query, a rich `message` object, and no
* `spokenTextMode` / `extraExtensionParameters` / `isSbsSupported` /
* `renderReferencesBehindEOS` / `disconnectBehavior` — none of those are still
* observed on the wire, and the stale shape gets closed immediately after the
* invocation.
* Base shape from the #10718 recapture (populated `clientInfo` +
* `productThreadType:"Office"`, a `conversationId` matching the WS URL query, a
* rich `message` object), extended per a 2026-08-21 live capture that found the
* #10718 shape alone no longer produces a `type:1 target:"update"` frame — the
* socket only replies with keepalive pings and closes. The additions below
* (richer `clientInfo`, non-empty `plugins`, `extraExtensionParameters`,
* `isSbsSupported`, `renderReferencesBehindEOS`,
* `message.connectedFederatedConnections`, and `disconnectBehavior` on every
* tier) are exactly the fields the 2026-08-21 capture had that this shape was
* missing; the #10718 fields (`conversationId`, `productThreadType`,
* `toolChoice`, `message.attachments`) are kept as-is since removing them was
* not verified against a live socket.
*/
export function buildChatInvocation(opts: ChatInvocationOptions): Record<string, unknown> {
const clientInfo = {
clientAppName: "Office",
clientPlatform: "mcmcopilot-web",
clientEntrypoint: "mcmcopilot-officeweb",
clientSessionId: opts.sessionId,
ProductCategory: "Chat",
clientAppType: "Web",
productEntryPoint: "ChatPanel",
deviceOS: "Windows",
deviceType: "Desktop",
clientPlatformVersion: "10",
};
return {
type: 4,
target: "chat",
@@ -487,17 +555,17 @@ export function buildChatInvocation(opts: ChatInvocationOptions): Record<string,
? [...opts.allowedMessageTypes]
: [...ALLOWED_MESSAGE_TYPES],
clientCorrelationId: opts.clientCorrelationId ?? opts.traceId,
clientInfo: {
clientAppName: "Office",
clientPlatform: "mcmcopilot-web",
},
clientInfo,
conversationId: opts.conversationId,
extraExtensionParameters: {},
isStartOfSession: opts.isStartOfSession ?? true,
message: {
adaptiveCards: [],
attachments: null,
author: "user",
clientInfo,
clientPreferences: {},
connectedFederatedConnections: ["dummyId"],
entityAnnotationTypes: ["People", "File", "Event", "Email", "TeamsMessage"],
experienceType: "Default",
inputMethod: "Keyboard",
@@ -510,22 +578,27 @@ export function buildChatInvocation(opts: ChatInvocationOptions): Record<string,
requestId: opts.requestId,
text: opts.text,
},
isSbsSupported: true,
options: {},
optionsSets: opts.optionsSets ?? [...M365_DEFAULT_OPTION_SETS],
plugins: opts.plugins ?? [],
// 2026-08-21 capture (#11069): BingWebSearch is now the universal
// BuiltIn plugin on individual/consumer tier; keep an opt-out override.
plugins: opts.plugins ?? [{ Id: "BingWebSearch", Source: "BuiltIn" }],
...(opts.customInstructions ? { customInstructions: opts.customInstructions } : {}),
productThreadType: "Office",
renderReferencesBehindEOS: true,
sessionId: opts.sessionId,
sliceIds: [],
source: "officeweb",
streamingMode: "ConciseWithPadding",
threadLevelGptId: {},
tone: opts.tone ?? "magic",
// 2026-08-21 capture (#11069): tone is now capitalized "Magic" on both tiers.
tone: opts.tone ?? "Magic",
toolChoice: opts.toolChoice ?? null,
traceId: opts.traceId,
// #8971 keeps "continue" for the enterprise tier; the individual/EDU wire
// omits the key, so only include it when actually set (#10718).
...(opts.disconnectBehavior ? { disconnectBehavior: opts.disconnectBehavior } : {}),
// 2026-08-21 capture — disconnectBehavior:"continue" is sent on every
// tier now, not gated to enterprise as the #8971 comment described.
disconnectBehavior: opts.disconnectBehavior ?? "continue",
},
],
};

View File

@@ -430,6 +430,7 @@ export class GlmExecutor extends DefaultExecutor {
let response: Response;
try {
this.assertOutboundUrlAllowed(url); // GHSA-4f49: glm has its own fetch path
response = await fetch(url, {
method: "POST",
headers,

View File

@@ -33,7 +33,7 @@ import { createHash } from "node:crypto";
import { BaseExecutor, type ExecuteInput } from "./base.ts";
import { makeExecutorErrorResult as makeErrorResult, sanitizeErrorMessage } from "../utils/error.ts";
const BASE_URL = "https://www.hailuo.ai";
const BASE_URL = "https://chat.minimax.io";
const API_PATH = "/v4/api/chat/msg";
const USER_AGENT =
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36";

View File

@@ -471,6 +471,7 @@ export class NlpCloudExecutor extends BaseExecutor {
}
try {
this.assertOutboundUrlAllowed(url); // GHSA-4f49: nlpcloud has its own fetch path
const response = await fetch(url, {
method: "POST",
headers,

View File

@@ -1,6 +1,6 @@
import { BaseExecutor, type ExecuteInput, type ProviderCredentials } from "./base.ts";
import { PROVIDERS } from "../config/constants.ts";
import { getModelTargetFormat } from "../config/providerModels.ts";
import { getModelTargetFormat, PROVIDER_ID_TO_ALIAS } from "../config/providerModels.ts";
import {
injectReasoningContentForThinkingModel,
isThinkingMessageModel,
@@ -125,6 +125,24 @@ export function isPremiumOpencodeModel(model: string, provider: string): boolean
return !OPENCODE_FREE_MODELS.has(model);
}
/**
* Resolves the registry `targetFormat` for a model, aliasing `provider` first.
*
* `PROVIDER_MODELS` is keyed by the provider's public ALIAS (e.g. `"oc"`), not its
* raw registry id (e.g. `"opencode"`) — mirrors `resolveChatCoreTargetFormat()`
* (`handlers/chatCore/targetFormat.ts`), which already aliases before calling
* `getModelTargetFormat()`. Calling it with the raw id here made every entry miss
* silently (fell through to `"openai"`), while chatCore's own request-body
* translation (correctly aliased) still switched to the Responses API shape for
* `targetFormat:"openai-responses"` models — sending a Responses-shaped body to
* the `/chat/completions` URL this executor's own `buildUrl()` kept selecting.
* Exported for testability.
*/
export function resolveOpencodeTargetFormat(provider: string, model: string): string {
const alias = PROVIDER_ID_TO_ALIAS[provider] || provider;
return getModelTargetFormat(alias, model) || "openai";
}
export class OpencodeExecutor extends BaseExecutor {
/** Delegates to `isPremiumOpencodeModel`. Exported for testability. */
static isPremiumModel(model: string, provider: string): boolean {
@@ -193,7 +211,10 @@ export class OpencodeExecutor extends BaseExecutor {
return pickRotatableAccount(this.accounts, this);
}
private markCooldown(account: OpencodeAccountState, kind: "transient" | "terminal" = "transient"): void {
private markCooldown(
account: OpencodeAccountState,
kind: "transient" | "terminal" = "transient"
): void {
markAccountCooldown(account, kind);
}
@@ -202,7 +223,7 @@ export class OpencodeExecutor extends BaseExecutor {
}
async execute(input: ExecuteInput) {
this._requestFormat = getModelTargetFormat(this.provider, input.model) || "openai";
this._requestFormat = resolveOpencodeTargetFormat(this.provider, input.model);
// #8681: Gate premium opencode models behind a usable API key.
// When the connection is keyless (no apiKey, no accessToken) and the model

View File

@@ -10,16 +10,24 @@ import {
import { resolveMcpCallerApiKeyId } from "../mcpCallerIdentity.ts";
/**
* Resolve the memory owner id for an MCP tool call:
* explicit arg wins, otherwise fall back to the authenticated caller's
* principal id (HTTP auth headers on SSE/Streamable HTTP transports,
* OMNIROUTE_API_KEY env var on stdio). Keeps MCP-stored memories under
* the same owner id that chat-context memory uses, so retrieval in the
* chat pipeline finds entries written via MCP.
* Resolve the memory owner id for an MCP tool call.
*
* The authenticated caller's principal ALWAYS wins over a caller-supplied
* `apiKeyId` — otherwise any MCP caller could read, write, or delete another
* principal's memories by putting a different id in the tool arguments
* (GHSA-cpv3-xr7r-xf8q, IDOR). The caller is resolved from the per-request HTTP
* auth headers on SSE / Streamable HTTP transports, or from OMNIROUTE_API_KEY on
* stdio. The explicit argument is only honored as a fallback when no caller can
* be resolved (a bare local stdio process with no configured key — already
* trusted), preserving the local-tooling flow. Keeps MCP-stored memories under
* the same owner id that chat-context memory uses, so retrieval in the chat
* pipeline finds entries written via MCP.
*/
async function resolveMemoryOwnerId(explicit?: string): Promise<string> {
const caller = await resolveMcpCallerApiKeyId().catch(() => undefined);
if (caller) return caller;
if (explicit && explicit.trim() !== "") return explicit.trim();
return (await resolveMcpCallerApiKeyId().catch(() => undefined)) || "mcp";
return "mcp";
}
export const MemorySearchSchema = z.object({

View File

@@ -607,7 +607,7 @@ export async function prepareVirtualAutoComboInputs(
// remaining allowance as a percentage, and a raw ">0" comparison would
// let a reading of e.g. 0.3% (rounding noise, not real headroom) pass.
minRemainingAllowance: 1,
maxStateAgeMs: (settings.autoRefreshProviderQuotaInterval ?? 180) * 1000,
maxStateAgeMs: (Number(settings.autoRefreshProviderQuotaInterval) || 180) * 1000,
});
if (strictFilteredPool !== pool) pool = strictFilteredPool;

View File

@@ -128,6 +128,75 @@ function tryParseJson(raw: string): unknown {
}
}
/**
* Splits a tool_call `arguments` string that is actually multiple back-to-back JSON
* objects glued together with no separator, into its individual object substrings.
*
* Root cause (observed on opencode/muse-spark-1.2-contributor-free via the zen
* provider): some upstreams never vary `index`/`id` across a 2nd/3rd/… tool_call of
* the SAME name emitted in one turn, so every delta in `buildOpenAISummary` above
* resolves to the same accumulator key and `arguments` ends up as N JSON objects
* concatenated with no delimiter — invalid as a single JSON value, but each object is
* individually well-formed. Structural, not provider-specific: applies to whichever
* upstream exhibits the same index-collision streaming bug.
*
* Returns `null` when `raw` is empty, already valid single JSON, or does not scan as
* ≥2 back-to-back valid JSON values — callers must leave `arguments` untouched in
* that case (never regress a value that used to reach the client as-is).
*/
export function splitConcatenatedToolCallArguments(raw: string): string[] | null {
if (!raw) return null;
try {
JSON.parse(raw);
return null; // Already a single valid JSON value — nothing to split.
} catch {
// Fall through to the multi-value scan below.
}
const parts: string[] = [];
let depth = 0;
let inString = false;
let escaped = false;
let start = -1;
for (let i = 0; i < raw.length; i++) {
const ch = raw[i];
if (start === -1) {
if (ch === " " || ch === "\n" || ch === "\r" || ch === "\t") continue;
if (ch !== "{" && ch !== "[") return null; // Not a value boundary — bail, leave untouched.
start = i;
}
if (inString) {
if (escaped) escaped = false;
else if (ch === "\\") escaped = true;
else if (ch === '"') inString = false;
continue;
}
if (ch === '"') {
inString = true;
continue;
}
if (ch === "{" || ch === "[") depth++;
else if (ch === "}" || ch === "]") {
depth--;
if (depth === 0) {
parts.push(raw.slice(start, i + 1));
start = -1;
}
}
}
if (start !== -1 || depth !== 0 || parts.length < 2) return null;
for (const part of parts) {
try {
JSON.parse(part);
} catch {
return null; // One of the scanned segments isn't valid JSON — bail entirely.
}
}
return parts;
}
// ─── Per-format live reducers ────────────────────────────────────────────────
// Each reducer mirrors the corresponding build*Summary()'s original for-loop
// body exactly (ingest = one loop iteration, finalize = the post-loop return),
@@ -262,7 +331,27 @@ function createOpenAIReducer(fallbackModel?: string | null): SummaryReducer {
message.reasoning_content = joinedReasoning;
}
const finalToolCalls = [...toolCalls.values()].sort((a, b) => a.index - b.index);
const mergedToolCalls = [...toolCalls.values()].sort((a, b) => a.index - b.index);
// Expand any entry whose accumulated `arguments` turned out to be multiple
// concatenated JSON objects (upstream never varied index/id across repeated
// same-name tool_calls) into its own separate tool_calls entries.
const finalToolCalls: ToolCall[] = [];
let nextIndex = 0;
for (const tc of mergedToolCalls) {
const splitArgs = splitConcatenatedToolCallArguments(tc.function.arguments);
if (!splitArgs) {
finalToolCalls.push({ ...tc, index: nextIndex++ });
continue;
}
for (const [i, args] of splitArgs.entries()) {
finalToolCalls.push({
id: tc.id ? `${tc.id}_split${i}` : null,
index: nextIndex++,
type: tc.type,
function: { name: tc.function.name, arguments: args },
});
}
}
if (finalToolCalls.length > 0) {
finishReason = "tool_calls";
message.tool_calls = finalToolCalls;

View File

@@ -131,12 +131,12 @@ function runNextBuild() {
}
export function resolveNextBuildBundlerFlag(baseEnv = process.env) {
// Turbopack is the default production bundler (Next 16 stable). Benchmarked on
// this codebase: 2-3x faster than the single-threaded webpack pass (17min -> 9min
// on a 32-core box; ~20min -> 7min on ubuntu-latest), artifact validated
// end-to-end (standalone smoke + e2e/package/electron CI jobs). Webpack stays as
// the explicit escape hatch (=0) for bundler-compat regressions.
return baseEnv.OMNIROUTE_USE_TURBOPACK === "0" ? "--webpack" : "--turbopack";
// Turbopack is the default on Node.js; on Bun or when explicitly disabled (=0),
// use Webpack (--webpack) to avoid Turbopack V8 internal worker API mismatches.
if (process.versions.bun || baseEnv.OMNIROUTE_USE_TURBOPACK === "0") {
return "--webpack";
}
return "--turbopack";
}
/**

View File

@@ -15,6 +15,12 @@ if (!support.nodeCompatible) {
process.exit(1);
}
console.log(
`Node.js ${support.nodeVersion} satisfies OmniRoute secure runtime policy (${support.supportedRange}).`
);
if (process.versions.bun) {
console.log(
`Bun ${process.versions.bun} (${support.nodeVersion}) satisfies OmniRoute secure runtime policy.`
);
} else {
console.log(
`Node.js ${support.nodeVersion} satisfies OmniRoute secure runtime policy (${support.supportedRange}).`
);
}

View File

@@ -83,8 +83,10 @@ const { dashboardPort } = runtimePorts;
const hostname = process.env.HOST || "0.0.0.0";
// Turbopack by default in dev (matches the Next 16 CLI default and the production
// build default in build-next-isolated.mjs); OMNIROUTE_USE_TURBOPACK=0 is the
// webpack escape hatch.
const useTurbopack = dev && mergedEnv.OMNIROUTE_USE_TURBOPACK !== "0";
// webpack escape hatch. Under Bun, Turbopack native V8 bindings are unavailable,
// so Bun automatically disables Turbopack and uses Webpack.
const isBun = Boolean(process.versions.bun);
const useTurbopack = dev && mergedEnv.OMNIROUTE_USE_TURBOPACK !== "0" && !isBun;
process.env.OMNIROUTE_WS_BRIDGE_SECRET ||= randomUUID();
// Per-process secret used to prove the trusted peer-IP stamp came from this
// server (read by the authz middleware in the same process). See peer-stamp.mjs.

View File

@@ -12,8 +12,13 @@ import {
} from "@/lib/combos/intelligentRouting";
import { AI_PROVIDERS } from "@/shared/constants/providers";
function getI18nOrFallback(t: any, key: string, fallback: string) {
if (typeof t?.has === "function" && t.has(key)) return t(key);
function getI18nOrFallback(
t: any,
key: string,
fallback: string,
values?: Record<string, unknown>
) {
if (typeof t?.has === "function" && t.has(key)) return t(key, values);
return fallback;
}
@@ -94,10 +99,9 @@ export default function IntelligentComboPanel({
const updatedCombo = await response.json();
onComboUpdated?.(updatedCombo);
notify.success(
getI18nOrFallback(t, "modePackUpdated", "Mode pack updated to {pack}.").replace(
"{pack}",
modePackId
)
getI18nOrFallback(t, "modePackUpdated", "Mode pack updated to {pack}.", {
pack: modePackId,
}).replace("{pack}", modePackId)
);
} catch (error: any) {
notify.error(error?.message || "Failed to update mode pack.");
@@ -184,10 +188,9 @@ export default function IntelligentComboPanel({
</div>
{savingModePack && (
<span className="text-[11px] text-text-muted">
{getI18nOrFallback(t, "savingModePack", "Saving {pack}…").replace(
"{pack}",
savingModePack
)}
{getI18nOrFallback(t, "savingModePack", "Saving {pack}…", {
pack: savingModePack,
}).replace("{pack}", savingModePack)}
</span>
)}
</div>

View File

@@ -533,9 +533,9 @@ function getStrategyBadgeClass(strategy) {
return "bg-blue-500/15 text-blue-600 dark:text-blue-400";
}
function getI18nOrFallback(t, key, fallback) {
function getI18nOrFallback(t, key, fallback, values) {
try {
if (typeof t.has === "function" && t.has(key)) return t(key);
if (typeof t.has === "function" && t.has(key)) return t(key, values);
} catch {}
return fallback;
}
@@ -1565,7 +1565,8 @@ function StrategyRecommendationsPanel({ strategy, onApply, showNudge }) {
{getI18nOrFallback(
t,
"recommendationsUpdated",
"Recommendations updated for {strategy}."
"Recommendations updated for {strategy}.",
{ strategy: strategyLabel }
).replace("{strategy}", strategyLabel)}
</div>
)}

View File

@@ -0,0 +1,165 @@
"use client";
import { useRef, useState } from "react";
import { useTranslations } from "next-intl";
import {
extractM365CredentialFromHar,
describeHarImportExpiry,
type M365HarImportResult,
} from "@/shared/utils/m365HarImport";
import { providerText, type ProviderMessageTranslator } from "../providerPageHelpers";
type HarImporter = (text: string) => M365HarImportResult;
// One entry per web-session provider that can offer HAR import. Add a new
// key here (and its own extractor in src/shared/utils/) to support another
// provider — the button renders nothing for any provider not listed.
const HAR_IMPORTERS: Record<string, HarImporter> = {
"copilot-m365-web": extractM365CredentialFromHar,
};
const ERROR_MESSAGE_KEYS: Record<string, [string, string]> = {
notJson: ["harImportErrorNotJson", "That file isn't valid JSON — is it really a .har export?"],
noEntries: ["harImportErrorNoEntries", "This HAR has no network entries recorded."],
noChathubUrl: [
"harImportErrorNoChathubUrl",
"No Copilot chat connection found in this HAR. Send at least one chat message in m365.cloud.microsoft before exporting.",
],
unparsableUrl: [
"harImportErrorUnparsableUrl",
"Found the chat connection, but couldn't read its URL.",
],
missingFields: [
"harImportErrorMissingFields",
"Found the chat connection, but the token was missing from it.",
],
};
export interface HarImportButtonProps {
provider: string;
onImport: (apiKey: string) => void;
}
export default function HarImportButton({ provider, onImport }: HarImportButtonProps) {
const t = useTranslations("providers") as ProviderMessageTranslator;
const importer = HAR_IMPORTERS[provider];
const fileInputRef = useRef<HTMLInputElement>(null);
const [state, setState] = useState<
| { phase: "idle" }
| { phase: "reading" }
| { phase: "error"; message: string }
| { phase: "success"; expiresAt: number | null }
>({ phase: "idle" });
if (!importer) return null;
async function handleFile(file: File | undefined) {
if (!file) return;
setState({ phase: "reading" });
let text: string;
try {
text = await file.text();
} catch {
setState({
phase: "error",
message: providerText(t, "harImportErrorReadFailed", "Couldn't read that file."),
});
return;
}
const result = importer(text);
if (!result.ok) {
const [key, fallback] = ERROR_MESSAGE_KEYS[result.error] ?? [
"harImportErrorUnknown",
"Couldn't extract a credential from that HAR file.",
];
setState({ phase: "error", message: providerText(t, key, fallback) });
return;
}
onImport(result.apiKey);
setState({ phase: "success", expiresAt: result.expiresAt });
}
const expiry = state.phase === "success" ? describeHarImportExpiry(state.expiresAt) : null;
const expiryText =
expiry?.tone === "unknown"
? providerText(t, "harImportStatusUnknownExpiry", "Imported. Couldn't read its expiry.")
: expiry?.tone === "bad"
? providerText(
t,
"harImportStatusExpired",
"Imported, but this token already expired ({minutes}m ago) — export a fresh HAR.",
{ minutes: Math.abs(expiry.minutesRemaining ?? 0) }
)
: expiry?.tone === "warn"
? providerText(
t,
"harImportStatusExpiringSoon",
"Imported — valid for only ~{minutes}m more.",
{ minutes: expiry.minutesRemaining ?? 0 }
)
: expiry?.tone === "ok"
? providerText(t, "harImportStatusValid", "Imported — valid for ~{minutes}m.", {
minutes: expiry.minutesRemaining ?? 0,
})
: null;
return (
<div className="flex flex-col gap-1.5">
<div className="flex items-center gap-2">
<button
type="button"
onClick={() => fileInputRef.current?.click()}
disabled={state.phase === "reading"}
data-testid="har-import-button"
className="inline-flex items-center gap-1.5 rounded border border-border px-2.5 py-1.5 text-xs font-medium text-text-main hover:bg-surface-hover disabled:opacity-50"
>
<span className="material-symbols-outlined text-[16px]" aria-hidden="true">
upload_file
</span>
{state.phase === "reading"
? providerText(t, "harImportButtonBusy", "Importing…")
: providerText(t, "harImportButtonLabel", "Import .har file")}
</button>
<span className="text-xs text-text-muted">
{providerText(
t,
"harImportButtonHint",
"Export from DevTools Network tab after sending at least one chat message."
)}
</span>
<input
ref={fileInputRef}
type="file"
accept=".har,application/json"
data-testid="har-import-input"
className="hidden"
onChange={(event) => {
void handleFile(event.target.files?.[0]);
event.target.value = "";
}}
/>
</div>
{state.phase === "error" && (
<p className="text-xs text-red-600 dark:text-red-400" data-testid="har-import-error">
{state.message}
</p>
)}
{state.phase === "success" && expiryText && (
<p
className={
expiry?.tone === "bad"
? "text-xs text-red-600 dark:text-red-400"
: expiry?.tone === "warn"
? "text-xs text-amber-700 dark:text-amber-300"
: "text-xs text-emerald-700 dark:text-emerald-300"
}
data-testid="har-import-status"
>
{expiryText}
</p>
)}
</div>
);
}

View File

@@ -31,6 +31,7 @@ import {
import { getWebSessionCredentialRequirement } from "../../webSessionCredentials";
import { useOpenRouterPresetControl } from "../OpenRouterPresetInput";
import WebSessionCredentialGuide from "../WebSessionCredentialGuide";
import HarImportButton from "../HarImportButton";
import CcCompatibleRequestDefaultsFields from "./CcCompatibleRequestDefaultsFields";
import { buildAddProviderSpecificData } from "./connectionProviderSpecificData";
import { getCommandCodeAuthPhaseLabel } from "./commandCodeAuthPhase";
@@ -155,7 +156,7 @@ export default function AddApiKeyModal({
if (!isOpen || wasOpen) return;
// On open, reset baseUrl and assign a unique default name so a second API key
// for the same provider doesn't reuse "main" and trigger the backend
// name-based upsert that would silently overwrite the first connection (#6499).
// name-based upsert that would silently overwrite the first connection (#6499, #11033).
setFormData((current) => ({
...current,
name: computeConnectionDefaultName(existingConnectionCount),
@@ -209,13 +210,13 @@ export default function AddApiKeyModal({
? "Freebuff uses an authentic CLI auth token obtained via codebuff CLI login or automated harvester."
: isWebSessionCredential
? getWebSessionCredentialHint(t, webSessionCredential, providerDisplayName, false)
: isLocalSelfHostedProvider
? t("localProviderApiKeyOptionalHint", {
provider: localProviderMetadata?.name || providerName || provider || "",
})
: apiKeyOptional
? t("apiKeyOptionalHint")
: undefined;
: isLocalSelfHostedProvider
? t("localProviderApiKeyOptionalHint", {
provider: localProviderMetadata?.name || providerName || provider || "",
})
: apiKeyOptional
? t("apiKeyOptionalHint")
: undefined;
const credentialValidationFailedMessage = isWebSessionCredential
? providerText(
t,
@@ -750,6 +751,12 @@ export default function AddApiKeyModal({
t={t}
/>
)}
{provider && (
<HarImportButton
provider={provider}
onImport={(apiKey) => setFormData({ ...formData, apiKey })}
/>
)}
{!isNoAuthWebSessionCredential && (
<div className="flex gap-2">
<Input
@@ -757,6 +764,12 @@ export default function AddApiKeyModal({
type="password"
value={formData.apiKey}
onChange={(e) => setFormData({ ...formData, apiKey: e.target.value })}
onKeyDown={(e) => {
if (e.key === "Enter" && !validating && !saving) {
e.preventDefault();
handleValidate();
}
}}
className="flex-1"
placeholder={apiCredentialPlaceholder}
hint={apiCredentialHint}

View File

@@ -49,6 +49,7 @@ import {
import { getWebSessionCredentialRequirement } from "../../webSessionCredentials";
import { useOpenRouterPresetControl } from "../OpenRouterPresetInput";
import WebSessionCredentialGuide from "../WebSessionCredentialGuide";
import HarImportButton from "../HarImportButton";
import CcCompatibleRequestDefaultsFields from "./CcCompatibleRequestDefaultsFields";
import { CodexConnectionFields } from "./CodexFingerprintFields";
import { assignEditApiKeyProviderSpecificData } from "./connectionProviderSpecificData";
@@ -909,6 +910,12 @@ export default function EditConnectionModal({
t={t}
/>
)}
{provider && (
<HarImportButton
provider={provider}
onImport={(apiKey) => setFormData({ ...formData, apiKey })}
/>
)}
{!isNoAuthWebSessionCredential && (
<div className="flex gap-2">
<Input

View File

@@ -4,7 +4,23 @@
// connection. Deriving a unique default from the existing connection count keeps
// the first connection ("main") backward-compatible while giving each subsequent
// one a distinct name ("main-2", "main-3", …).
export function computeConnectionDefaultName(existingConnectionCount?: number): string {
const count = existingConnectionCount ?? 0;
export function computeConnectionDefaultName(
existingConnectionCountOrConnections?: number | string[] | { name?: string }[]
): string {
if (Array.isArray(existingConnectionCountOrConnections)) {
const names = new Set(
existingConnectionCountOrConnections
.map((item) => (typeof item === "string" ? item : item?.name ?? ""))
.filter(Boolean)
);
if (!names.has("main")) return "main";
let index = 2;
while (names.has(`main-${index}`)) {
index++;
}
return `main-${index}`;
}
const count = existingConnectionCountOrConnections ?? 0;
return count <= 0 ? "main" : `main-${count + 1}`;
}

View File

@@ -17,6 +17,8 @@ import { logRoutingDecision } from "@/lib/a2a/routingLogger";
import { createA2AStream, SSE_HEADERS } from "@/lib/a2a/streaming";
import { A2A_SKILL_HANDLERS, executeA2ATaskWithState } from "@/lib/a2a/taskExecution";
import { getSettings } from "@/lib/db/settings";
import { isRequireApiKeyEnabled } from "@/shared/utils/featureFlags";
import { extractApiKey, isValidApiKey } from "@/sse/services/auth";
// ============ A2A v1.0 ↔ v0.3 compatibility layer ============
// A2A 1.0 renamed the JSON-RPC methods (message/send → SendMessage,
@@ -136,14 +138,25 @@ function tokensMatch(provided: string, expected: string): boolean {
return timingSafeEqual(a, b);
}
function authenticate(req: NextRequest): boolean {
// If no API key is configured, allow all requests
const configuredKey = process.env.OMNIROUTE_API_KEY;
if (!configuredKey) return true;
async function authenticate(req: NextRequest): Promise<boolean> {
// /a2a is outside the authz proxy matcher, so the REQUIRE_API_KEY posture the
// pipeline enforces for /v1 never ran here — the route accepted every caller
// whenever OMNIROUTE_API_KEY was unset, which is the shipped default
// (GHSA-v54m-6rm3-p565). Apply the same posture directly: when a client key is
// required, demand a valid OmniRoute key; otherwise honor the legacy explicit
// A2A key; otherwise stay keyless (the same local-first default as /v1).
const apiKey = extractApiKey(req);
if (isRequireApiKeyEnabled()) {
return apiKey ? await isValidApiKey(apiKey) : false;
}
const authHeader = req.headers.get("authorization") || "";
const token = authHeader.replace(/^Bearer\s+/i, "");
return tokensMatch(token, configuredKey);
const configuredKey = process.env.OMNIROUTE_API_KEY;
if (configuredKey) {
return apiKey ? tokensMatch(apiKey, configuredKey) : false;
}
// No API key required and none configured — allow (keyless local-first).
return true;
}
// ============ JSON-RPC Helpers ============
@@ -179,7 +192,7 @@ async function rejectIfA2ADisabled(id: string | number | null) {
export async function POST(req: NextRequest) {
// Auth check
if (!authenticate(req)) {
if (!(await authenticate(req))) {
return jsonRpcError(null, -32600, "Unauthorized: missing or invalid API key");
}

View File

@@ -1,5 +1,7 @@
import { NextResponse } from "next/server";
import { z } from "zod";
export const dynamic = "force-dynamic";
import {
type CliAgentInfo,
detectInstalledAgents,

View File

@@ -1,4 +1,5 @@
import { NextResponse } from "next/server";
export const dynamic = "force-dynamic";
import { getAllRateLimitStatus } from "@omniroute/open-sse/services/rateLimitManager.ts";
import {
getStats as getSemaphoreStats,

View File

@@ -6,6 +6,7 @@
*/
import { NextResponse } from "next/server";
export const dynamic = "force-dynamic";
import { getCompressionAnalyticsSummary } from "@/lib/db/compressionAnalytics";
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";

View File

@@ -1,4 +1,5 @@
import { NextResponse } from "next/server";
export const dynamic = "force-dynamic";
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
import { issueDashboardCsrfToken } from "@/server/authz/csrf";

View File

@@ -1,4 +1,5 @@
import { NextResponse } from "next/server";
export const dynamic = "force-dynamic";
import { cookies } from "next/headers";
import { jwtVerify } from "jose";

View File

@@ -1,4 +1,5 @@
import { NextResponse } from "next/server";
export const dynamic = "force-dynamic";
import { getBatch } from "@/lib/localDb";
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";

View File

@@ -1,4 +1,5 @@
import { NextResponse } from "next/server";
export const dynamic = "force-dynamic";
import { listBatches } from "@/lib/localDb";
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";

View File

@@ -1,4 +1,5 @@
import { NextRequest, NextResponse } from "next/server";
export const dynamic = "force-dynamic";
import { isAuthenticated } from "@/shared/utils/apiAuth";
import {
listSemanticCacheEntries,

View File

@@ -1,4 +1,5 @@
import { NextRequest, NextResponse } from "next/server";
export const dynamic = "force-dynamic";
import { isAuthenticated } from "@/shared/utils/apiAuth";
import {
clearReasoningCacheAll,

View File

@@ -1,4 +1,5 @@
import { NextRequest, NextResponse } from "next/server";
export const dynamic = "force-dynamic";
import {
getCacheStats,
clearCache,

View File

@@ -1,4 +1,5 @@
import { NextRequest, NextResponse } from "next/server";
export const dynamic = "force-dynamic";
import { clearMemoryCache, getMemoryCacheStats } from "@/lib/semanticCache";
import { isAuthenticated } from "@/shared/utils/apiAuth";
import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/error.ts";

View File

@@ -38,7 +38,14 @@ export async function GET(request: NextRequest) {
const url = new URL(request.url);
const scope: LeaderboardScope = (url.searchParams.get("scope") || "global") as LeaderboardScope;
const limit = Number(url.searchParams.get("limit") || 100);
const rawLimit = url.searchParams.get("limit");
const limit = rawLimit === null ? 100 : Number(rawLimit);
if (!Number.isInteger(limit) || limit < 1 || limit > 200) {
return NextResponse.json(
{ error: "'limit' must be an integer between 1 and 200" },
{ status: 400, headers: CORS_HEADERS }
);
}
const entries = await getTopN(scope, limit);

View File

@@ -18,10 +18,18 @@ export async function GET(request: NextRequest) {
const url = new URL(request.url);
const scope = (url.searchParams.get("scope") || "global") as LeaderboardScope;
const limit = Number(url.searchParams.get("limit") || 50);
const rawLimit = url.searchParams.get("limit");
const limit = rawLimit === null ? 50 : Number(rawLimit);
const apiKeyId = url.searchParams.get("apiKeyId");
const entries = await getTopN(scope, Math.min(limit, 200));
if (!Number.isInteger(limit) || limit < 1 || limit > 200) {
return NextResponse.json(
{ error: "'limit' must be an integer between 1 and 200" },
{ status: 400, headers: CORS_HEADERS }
);
}
const entries = await getTopN(scope, limit);
let myRank: number | null = null;
let neighbors = null;

View File

@@ -5,6 +5,7 @@ import { readRunningBuildSha } from "@/lib/monitoring/buildSha";
import { APP_CONFIG } from "@/shared/constants/config";
import { AI_PROVIDERS } from "@/shared/constants/providers";
import { isAuthenticated } from "@/shared/utils/apiAuth";
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
/**
* GET /api/monitoring/health — System health overview
@@ -20,10 +21,25 @@ import { isAuthenticated } from "@/shared/utils/apiAuth";
let healthPayloadCache: { payload: unknown; expiresAt: number } | null = null;
const HEALTH_PAYLOAD_TTL_MS = 1000;
export async function GET() {
// GHSA-mvf8-qc78-5mxm: the full health payload fingerprints the host (version,
// node version, pid, memory, provider config). An anonymous caller — the common
// case on a keyless install, and what a liveness/load-balancer probe needs — gets
// only the liveness verdict; the detail is reserved for a management principal.
function publicHealthView(payload: unknown): Record<string, unknown> {
const p = (payload ?? {}) as Record<string, unknown>;
return {
status: p.status ?? "unknown",
...(p.setupComplete !== undefined ? { setupComplete: p.setupComplete } : {}),
};
}
export async function GET(request: Request) {
const fullView = (await requireManagementAuth(request, { alwaysRequireAuth: true })) === null;
const cachedNow = Date.now();
if (healthPayloadCache && cachedNow <= healthPayloadCache.expiresAt) {
return NextResponse.json(healthPayloadCache.payload);
return NextResponse.json(
fullView ? healthPayloadCache.payload : publicHealthView(healthPayloadCache.payload)
);
}
const readHealthValue = <T>(label: string, reader: () => T, fallback: T): T => {
@@ -187,7 +203,7 @@ export async function GET() {
});
healthPayloadCache = { payload, expiresAt: Date.now() + HEALTH_PAYLOAD_TTL_MS };
return NextResponse.json(payload);
return NextResponse.json(fullView ? payload : publicHealthView(payload));
} catch (error) {
console.error("[API] GET /api/monitoring/health error:", error);
return NextResponse.json({

View File

@@ -24,6 +24,7 @@ import {
} from "@/models";
import { getConsistentMachineId } from "@/shared/utils/machineId";
import { isValidGheUrl } from "@/shared/validation/providerSpecificData";
import { AWS_REGION_PATTERN } from "@/lib/oauth/constants/oauth";
import { syncToCloud } from "@/lib/cloudSync";
import { startLocalServer } from "@/lib/oauth/utils/server";
import { runWithProxyContextOrDirect } from "@omniroute/open-sse/utils/proxyFetch.ts";
@@ -221,6 +222,16 @@ export async function GET(
(requestDeviceCode as any)(provider, null, providerOverrideConfig)
);
} else if ((provider === "kiro" || provider === "amazon-q") && startUrl) {
// GHSA-7x63: `region` is interpolated into the AWS OIDC endpoint URLs
// below, which requestDeviceCode() then fetches. Validate it against the
// canonical AWS region shape before it can steer the outbound host to an
// attacker-chosen target (userinfo/fragment tricks → SSRF / metadata).
if (!AWS_REGION_PATTERN.test(region)) {
return NextResponse.json(
{ error: "region must be a valid AWS region (e.g. us-east-1)" },
{ status: 400 }
);
}
const providerOverrideConfig = {
...providerData.config,
startUrl,

View File

@@ -3,7 +3,7 @@ import path from "path";
import { NextResponse } from "next/server";
import { createProviderConnection } from "@/models";
import { isAuthRequired, isAuthenticated } from "@/shared/utils/apiAuth";
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/error";
import {
scanCliProxyAuthDir,
@@ -23,9 +23,9 @@ function cliProxyConfigDir(): string {
}
async function requireImportAuth(request: Request) {
if (!(await isAuthRequired(request))) return null;
if (await isAuthenticated(request)) return null;
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
// GHSA-mg76: importing a provider connection is a state-mutating admin action;
// require management scope (or a dashboard session), not any valid client key.
return requireManagementAuth(request, { invalidApiKeyStatus: 401 });
}
export async function GET(request: Request) {

View File

@@ -3,7 +3,7 @@ import { z } from "zod";
import { extractCodexAccountInfo } from "@/lib/oauth/services/codexImport";
import { parseCodexSessionJson } from "@/lib/oauth/utils/codexSessionImport";
import { createProviderConnection } from "@/models";
import { isAuthRequired, isAuthenticated } from "@/shared/utils/apiAuth";
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
import { buildErrorBody, sanitizeErrorMessage } from "@omniroute/open-sse/utils/error.ts";
/**
@@ -93,10 +93,11 @@ async function parseRequestBody(
return { ok: true, resolved: resolved.resolved };
}
async function requireAuth(request: Request): Promise<NextResponse | null> {
if (!(await isAuthRequired(request))) return null;
if (await isAuthenticated(request)) return null;
return NextResponse.json(buildErrorBody(401, "Unauthorized"), { status: 401 });
async function requireAuth(request: Request): Promise<Response | null> {
// GHSA-mg76: importing a provider connection is a state-mutating admin action.
// Require management scope (or a dashboard session) rather than accepting any
// valid client key, which the PUBLIC /api/oauth/ classification otherwise allows.
return requireManagementAuth(request, { invalidApiKeyStatus: 401 });
}
export async function POST(request: Request) {

View File

@@ -2,7 +2,7 @@ import { NextResponse } from "next/server";
import { z } from "zod";
import { normalizeCodexImportRecord, flattenCodexImportPayload } from "@/lib/oauth/services/codexImport";
import { createProviderConnection } from "@/models";
import { isAuthRequired, isAuthenticated } from "@/shared/utils/apiAuth";
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/error.ts";
import { refreshCodexToken, isUnrecoverableRefreshError } from "@omniroute/open-sse/services/tokenRefresh.ts";
@@ -82,10 +82,10 @@ const bodySchema = z.object({
}),
});
async function requireAuth(request: Request): Promise<NextResponse | null> {
if (!(await isAuthRequired(request))) return null;
if (await isAuthenticated(request)) return null;
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
async function requireAuth(request: Request): Promise<Response | null> {
// GHSA-mg76: importing a provider connection is a state-mutating admin action;
// require management scope (or a dashboard session), not any valid client key.
return requireManagementAuth(request, { invalidApiKeyStatus: 401 });
}
export async function POST(request: Request) {

View File

@@ -1,5 +1,5 @@
import { NextResponse } from "next/server";
import { isAuthRequired, isAuthenticated } from "@/shared/utils/apiAuth";
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
import { tryAgentAuth, tryIdeAuth } from "@/lib/cursor/tokenExtractor";
/**
@@ -11,11 +11,9 @@ import { tryAgentAuth, tryIdeAuth } from "@/lib/cursor/tokenExtractor";
* 🔒 Auth-guarded: requires JWT cookie or Bearer API key (finding #258-4).
*/
export async function GET(request: Request) {
if (await isAuthRequired(request)) {
if (!(await isAuthenticated(request))) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
}
// GHSA-mg76 / GHSA-gxv4: reading/importing host credentials is a management action.
const authError = await requireManagementAuth(request, { invalidApiKeyStatus: 401 });
if (authError) return authError;
try {
// Try Cursor IDE first (has both accessToken and machineId)

View File

@@ -6,15 +6,15 @@ import { isCloudEnabled } from "@/models";
import { syncToCloud } from "@/lib/cloudSync";
import { cursorImportSchema } from "@/shared/validation/schemas";
import { isValidationFailure, validateBody } from "@/shared/validation/helpers";
import { isAuthRequired, isAuthenticated } from "@/shared/utils/apiAuth";
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
import { getConsistentMachineId } from "@/shared/utils/machineId";
import { runWithProxyContext } from "@omniroute/open-sse/utils/proxyFetch.ts";
import { resolveProxyForProvider } from "@/models";
async function requireOAuthImportAuth(request: Request) {
if (!(await isAuthRequired(request))) return null;
if (await isAuthenticated(request)) return null;
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
// GHSA-mg76: importing a provider connection is a state-mutating admin action;
// require management scope (or a dashboard session), not any valid client key.
return requireManagementAuth(request, { invalidApiKeyStatus: 401 });
}
/**

View File

@@ -1,7 +1,7 @@
import { NextResponse } from "next/server";
import { homedir } from "os";
import { join } from "path";
import { isAuthRequired, isAuthenticated } from "@/shared/utils/apiAuth";
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
import {
createProviderConnection,
getProviderConnections,
@@ -31,11 +31,9 @@ import {
* 🔒 Auth-guarded: requires JWT cookie or Bearer API key.
*/
export async function GET(request: Request) {
if (await isAuthRequired(request)) {
if (!(await isAuthenticated(request))) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
}
// GHSA-mg76 / GHSA-gxv4: reading/importing host credentials is a management action.
const authError = await requireManagementAuth(request, { invalidApiKeyStatus: 401 });
if (authError) return authError;
const { searchParams } = new URL(request.url);
const targetProvider = searchParams.get("targetProvider") === "amazon-q" ? "amazon-q" : "kiro";

View File

@@ -11,7 +11,7 @@ import { getConsistentMachineId } from "@/shared/utils/machineId";
import { syncToCloud } from "@/lib/cloudSync";
import { kiroImportSchema } from "@/shared/validation/schemas";
import { isValidationFailure, validateBody } from "@/shared/validation/helpers";
import { isAuthRequired, isAuthenticated } from "@/shared/utils/apiAuth";
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
import { runWithProxyContext } from "@omniroute/open-sse/utils/proxyFetch.ts";
import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/error";
import { findKiroConnectionByIdentity } from "@/lib/oauth/kiroConnectionIdentity";
@@ -38,9 +38,9 @@ export function buildKiroImportError(error: unknown): string {
}
async function requireOAuthImportAuth(request: Request) {
if (!(await isAuthRequired(request))) return null;
if (await isAuthenticated(request)) return null;
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
// GHSA-mg76: importing a provider connection is a state-mutating admin action;
// require management scope (or a dashboard session), not any valid client key.
return requireManagementAuth(request, { invalidApiKeyStatus: 401 });
}
async function upsertImportedKiroConnection(

View File

@@ -14,14 +14,14 @@ import {
extractLocalRaycastCredentials,
isRaycastLocalExtractAvailable,
} from "@/lib/oauth/services/raycastLocal";
import { isAuthRequired, isAuthenticated } from "@/shared/utils/apiAuth";
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
import { resolveProxyForProvider } from "@/models";
import { runWithProxyContext } from "@omniroute/open-sse/utils/proxyFetch.ts";
async function requireOAuthImportAuth(request: Request) {
if (!(await isAuthRequired(request))) return null;
if (await isAuthenticated(request)) return null;
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
// GHSA-mg76: importing a provider connection is a state-mutating admin action;
// require management scope (or a dashboard session), not any valid client key.
return requireManagementAuth(request, { invalidApiKeyStatus: 401 });
}
export async function GET(request: Request) {

View File

@@ -11,14 +11,14 @@ import { createProviderConnection } from "@/models";
import { RaycastService } from "@/lib/oauth/services/raycast";
import { raycastImportSchema } from "@/shared/validation/schemas";
import { isValidationFailure, validateBody } from "@/shared/validation/helpers";
import { isAuthRequired, isAuthenticated } from "@/shared/utils/apiAuth";
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
import { resolveProxyForProvider } from "@/models";
import { runWithProxyContext } from "@omniroute/open-sse/utils/proxyFetch.ts";
async function requireOAuthImportAuth(request: Request) {
if (!(await isAuthRequired(request))) return null;
if (await isAuthenticated(request)) return null;
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
// GHSA-mg76: importing a provider connection is a state-mutating admin action;
// require management scope (or a dashboard session), not any valid client key.
return requireManagementAuth(request, { invalidApiKeyStatus: 401 });
}
export async function POST(request: Request) {

View File

@@ -2,7 +2,7 @@ import { NextResponse } from "next/server";
import { createProviderConnection } from "@/models";
import { traeImportSchema } from "@/shared/validation/schemas";
import { isValidationFailure, validateBody } from "@/shared/validation/helpers";
import { isAuthRequired, isAuthenticated } from "@/shared/utils/apiAuth";
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
/**
* POST /api/oauth/trae/import
@@ -22,9 +22,9 @@ import { isAuthRequired, isAuthenticated } from "@/shared/utils/apiAuth";
* region — optional, default "US-East"
*/
async function requireOAuthImportAuth(request: Request) {
if (!(await isAuthRequired(request))) return null;
if (await isAuthenticated(request)) return null;
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
// GHSA-mg76: importing a provider connection is a state-mutating admin action;
// require management scope (or a dashboard session), not any valid client key.
return requireManagementAuth(request, { invalidApiKeyStatus: 401 });
}
export async function POST(request: Request) {

View File

@@ -28,6 +28,7 @@ import {
isAnthropicCompatibleProvider,
} from "@/shared/constants/providers";
import { isAuthenticated } from "@/shared/utils/apiAuth";
export const dynamic = "force-dynamic";
import { providerModelMutationSchema } from "@/shared/validation/schemas";
import { isValidationFailure, validateBody } from "@/shared/validation/helpers";

View File

@@ -1,4 +1,5 @@
import { NextResponse } from "next/server";
export const dynamic = "force-dynamic";
import { getAuditRequestContext, logAuditEvent } from "@/lib/compliance/index";
import {
getProviderAuditTarget,

View File

@@ -1,6 +1,7 @@
import { NextRequest, NextResponse } from "next/server";
import { z } from "zod";
import { isAuthenticated } from "@/shared/utils/apiAuth";
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
import { buildErrorBody } from "@omniroute/open-sse/utils/error";
import {
getObsidianSyncStatus,
@@ -21,10 +22,19 @@ export async function GET(request: NextRequest) {
try {
const status = await getObsidianSyncStatus();
// GHSA-62vw: the WebDAV password is reusable authentication material. Return
// the plaintext only to a genuine management principal (dashboard session or
// manage-scope key), never to an anonymous caller that reached this handler
// through the requireLogin=false open mode. The dashboard's authenticated
// reveal-password view is unaffected; anonymous callers get a set/unset flag.
const hasManagement =
(await requireManagementAuth(request, { alwaysRequireAuth: true })) === null;
return NextResponse.json({
webdavEnabled: status.webdavEnabled,
webdavUsername: status.webdavEnabled ? status.webdavUsername : null,
webdavPassword: status.webdavEnabled ? status.webdavPassword : null,
webdavPassword:
status.webdavEnabled && hasManagement ? status.webdavPassword : null,
webdavPasswordSet: status.webdavEnabled && Boolean(status.webdavPassword),
vaultPath: status.vaultPath,
});
} catch (error) {

View File

@@ -1,4 +1,5 @@
import { NextResponse } from "next/server";
export const dynamic = "force-dynamic";
import { getProviderById } from "@/shared/constants/providers";
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
import { getApiKeys } from "@/lib/db/apiKeys";

View File

@@ -1,4 +1,5 @@
import { NextResponse } from "next/server";
export const dynamic = "force-dynamic";
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
import { getCallLogs } from "@/lib/usageDb";
import { getCompletedDetails, getPendingById } from "@/lib/usage/usageHistory";

View File

@@ -5,6 +5,10 @@
* returning combo metadata to API-key callers. Kept in a separate module so
* the projection can be unit-tested without spinning up the Next.js route.
*
* #10968: the projection also reports `accountPinned` per model step — a boolean
* derived from the stripped `connectionId`, so callers can distinguish a combo
* that fails over between two accounts of one provider from a duplicated step.
*
* #3979: client-facing combo catalogs (the `/v1/combos`, VS Code and LobeHub /
* OpenCode import surfaces) can opt into advertising the combo's resolved
* capabilities (multimodal / reasoning / caching) so importing clients enable
@@ -17,6 +21,19 @@ export interface PublicComboStep {
model?: string;
comboName?: string;
providerId?: string;
/**
* #10968: whether this step pins one specific account of its provider.
*
* Two steps that pin different accounts of the same provider project to
* identical `{kind, model, providerId}` objects, so a client cannot tell a
* two-account failover from the same step listed twice. This says which it
* is without exposing the `connectionId` the flag is derived from — not even
* a prefix, per the issue.
*
* Set on every `model` step. Absent on `combo-ref`, which routes through
* another combo and has no account of its own.
*/
accountPinned?: boolean;
}
/**
@@ -66,6 +83,10 @@ export function projectComboStep(step: Record<string, unknown>): PublicComboStep
if (typeof step.providerId === "string" && step.providerId.length > 0) {
out.providerId = step.providerId;
}
// Same shape test as providerId above. `cleanupComboConnectionRefs` drops the
// key when the connection is deleted, so a step whose pinned account is gone
// reports false rather than pointing at nothing.
out.accountPinned = typeof step.connectionId === "string" && step.connectionId.length > 0;
return out;
}
return null;

View File

@@ -68,7 +68,7 @@ async function postHandler(request, context) {
const response = await handleModeration({ body: { ...body, model }, credentials });
if (response?.ok) {
await clearRecoveredProviderState(credentials);
await clearRecoveredProviderState(credentials as Record<string, unknown>);
}
return response;
}

View File

@@ -15,22 +15,15 @@ import { encryptMetadata } from "@/lib/webhookDispatcher";
import { isEncryptionEnabled } from "@/lib/db/encryption";
import { parseAndValidateWebhookUrl } from "@/shared/network/outboundUrlGuardPolicy";
import { WEBHOOK_EVENT_VALUES } from "@/lib/webhooks/eventDescriptions";
const WEBHOOK_KINDS = ["slack", "telegram", "discord", "custom"] as const;
const WEBHOOK_EVENT_VALUES = [
"*",
"request.completed",
"request.failed",
"provider.error",
"provider.recovered",
"quota.exceeded",
"combo.switched",
"test.ping",
] as const;
const WEBHOOK_EVENT_VALUES_WITH_WILDCARD = ["*", ...WEBHOOK_EVENT_VALUES] as const;
const updateWebhookSchema = z
.object({
url: z.string().min(1).max(2000).optional(),
events: z.array(z.enum(WEBHOOK_EVENT_VALUES)).optional(),
events: z.array(z.enum(WEBHOOK_EVENT_VALUES_WITH_WILDCARD)).optional(),
secret: z.string().max(500).optional(),
description: z.string().max(1000).optional(),
enabled: z.boolean().optional(),

View File

@@ -14,12 +14,15 @@ import { encryptMetadata } from "@/lib/webhookDispatcher";
import { isEncryptionEnabled } from "@/lib/db/encryption";
import { parseAndValidateWebhookUrl } from "@/shared/network/outboundUrlGuardPolicy";
import { WEBHOOK_EVENT_VALUES } from "@/lib/webhooks/eventDescriptions";
const WEBHOOK_KINDS = ["slack", "telegram", "discord", "custom"] as const;
const WEBHOOK_EVENT_VALUES_WITH_WILDCARD = ["*", ...WEBHOOK_EVENT_VALUES] as const;
const createWebhookSchema = z
.object({
url: z.string().min(1).max(2000),
events: z.array(z.string()).optional().default(["*"]),
events: z.array(z.enum(WEBHOOK_EVENT_VALUES_WITH_WILDCARD)).optional().default(["*"]),
secret: z.string().max(500).optional(),
description: z.string().max(1000).optional().default(""),
kind: z.enum(WEBHOOK_KINDS).optional().default("custom"),

View File

@@ -2,4 +2,5 @@
* Kubernetes-style readiness alias of /healthz.
* Same lifecycle phase, same 200/503 bodies. Not a liveness probe.
*/
export { dynamic, GET, HEAD } from "../healthz/route";
export const dynamic = "force-dynamic";
export { GET, HEAD } from "../healthz/route";

View File

@@ -12202,7 +12202,7 @@
},
"omni-webhooks": {
"name": "خطافات الويب",
"description": "تسجيل، وعرض، واختبار، وإزالة نقاط نهاية webhook. تكوين اشتراكات الأحداث (request.completed، وprovider.error، وbudget.exceeded، إلخ) وإدارة محاولات التسليم."
"description": "__MISSING__:Register, list, test, and remove webhook endpoints. Configure event subscriptions (request.completed, request.failed, quota.exceeded, etc.) and manage delivery retries."
},
"omni-mcp": {
"name": "خادم MCP",

View File

@@ -12202,7 +12202,7 @@
},
"omni-webhooks": {
"name": "Vebhuklar",
"description": "Vebhuk son nöqtələrini qeydiyyatdan keçirin, siyahıya alın, sınaqdan keçirin və silin. Hadisə abunəliklərini (request.completed, provider.error, budget.exceeded və s.) konfiqurasiya edin və çatdırılmanın təkrar cəhdlərini idarə edin."
"description": "__MISSING__:Register, list, test, and remove webhook endpoints. Configure event subscriptions (request.completed, request.failed, quota.exceeded, etc.) and manage delivery retries."
},
"omni-mcp": {
"name": "MCP Serveri",

View File

@@ -12202,7 +12202,7 @@
},
"omni-webhooks": {
"name": "Уебхукове",
"description": "Register, list, test, and remove webhook endpoints. Configure event subscriptions (request.completed, provider.error, budget.exceeded, etc.) and manage delivery retries."
"description": "__MISSING__:Register, list, test, and remove webhook endpoints. Configure event subscriptions (request.completed, request.failed, quota.exceeded, etc.) and manage delivery retries."
},
"omni-mcp": {
"name": "MCP сървър",

View File

@@ -12202,7 +12202,7 @@
},
"omni-webhooks": {
"name": "ওয়েবহুক",
"description": "ওয়েবহুক এন্ডপয়েন্ট রেজিস্টার, তালিকাভুক্ত, পরীক্ষা এবং অপসারণ করুন। ইভেন্ট সাবস্ক্রিপশন (request.completed, provider.error, budget.exceeded ইত্যাদি) কনফিগার করুন এবং ডেলিভারি রিট্রাই পরিচালনা করুন।"
"description": "__MISSING__:Register, list, test, and remove webhook endpoints. Configure event subscriptions (request.completed, request.failed, quota.exceeded, etc.) and manage delivery retries."
},
"omni-mcp": {
"name": "MCP সার্ভার",

View File

@@ -12202,7 +12202,7 @@
},
"omni-webhooks": {
"name": "Webhooky",
"description": "Registrujte, zobrazujte, testujte a odebírejte koncové body webhooků. Konfigurujte odběry událostí (request.completed, provider.error, budget.exceeded atd.) a spravujte opakování doručení."
"description": "__MISSING__:Register, list, test, and remove webhook endpoints. Configure event subscriptions (request.completed, request.failed, quota.exceeded, etc.) and manage delivery retries."
},
"omni-mcp": {
"name": "MCP server",

View File

@@ -12202,7 +12202,7 @@
},
"omni-webhooks": {
"name": "Webhooks",
"description": "Registrer, list, test og fjern webhook-slutpunkter. Konfigurer begivenhedsabonnementer (request.completed, provider.error, budget.exceeded osv.) og administrer leveringsforsøg."
"description": "__MISSING__:Register, list, test, and remove webhook endpoints. Configure event subscriptions (request.completed, request.failed, quota.exceeded, etc.) and manage delivery retries."
},
"omni-mcp": {
"name": "MCP-server",

View File

@@ -12202,7 +12202,7 @@
},
"omni-webhooks": {
"name": "Webhooks",
"description": "Webhook-Endpunkte registrieren, auflisten, testen und entfernen. Ereignis-Abonnements (request.completed, provider.error, budget.exceeded etc.) konfigurieren und Zustellungsversuche verwalten."
"description": "__MISSING__:Register, list, test, and remove webhook endpoints. Configure event subscriptions (request.completed, request.failed, quota.exceeded, etc.) and manage delivery retries."
},
"omni-mcp": {
"name": "MCP-Server",

View File

@@ -6269,6 +6269,20 @@
"webSessionGuideStep3": "Copy the required credential from the provider's own domain. For cookies, copy only the Cookie header value and omit Cookie:.",
"webSessionGuideStep3Manual": "Manual path: open the browser developer tools (F12 → Network), refresh the page, open an authenticated request, and copy the Cookie header value from Request Headers — omit the Cookie: prefix.",
"webSessionGuideStep4": "Paste it here and check the connection. If it stops working, sign in again and replace it with a fresh value.",
"harImportButtonLabel": "Import .har file",
"harImportButtonBusy": "Importing…",
"harImportButtonHint": "Export from DevTools Network tab after sending at least one chat message.",
"harImportStatusValid": "Imported — valid for ~{minutes}m.",
"harImportStatusExpiringSoon": "Imported — valid for only ~{minutes}m more.",
"harImportStatusExpired": "Imported, but this token already expired ({minutes}m ago) — export a fresh HAR.",
"harImportStatusUnknownExpiry": "Imported. Couldn't read its expiry.",
"harImportErrorNotJson": "That file isn't valid JSON — is it really a .har export?",
"harImportErrorNoEntries": "This HAR has no network entries recorded.",
"harImportErrorNoChathubUrl": "No Copilot chat connection found in this HAR. Send at least one chat message in m365.cloud.microsoft before exporting.",
"harImportErrorUnparsableUrl": "Found the chat connection, but couldn't read its URL.",
"harImportErrorMissingFields": "Found the chat connection, but the token was missing from it.",
"harImportErrorReadFailed": "Couldn't read that file.",
"harImportErrorUnknown": "Couldn't extract a credential from that HAR file.",
"webSessionSecurityHint": "Treat this like a password: it may access your signed-in web account until it expires or is revoked.",
"webNoAuthGuideTitle": "No credential required",
"webNoAuthGuideBody": "{provider} does not need an API key or cookie. Save the connection to use its free web endpoint.",
@@ -12207,7 +12221,7 @@
},
"omni-webhooks": {
"name": "Webhooks",
"description": "Register, list, test, and remove webhook endpoints. Configure event subscriptions (request.completed, provider.error, budget.exceeded, etc.) and manage delivery retries."
"description": "Register, list, test, and remove webhook endpoints. Configure event subscriptions (request.completed, request.failed, quota.exceeded, etc.) and manage delivery retries."
},
"omni-mcp": {
"name": "MCP Server",

View File

@@ -12202,7 +12202,7 @@
},
"omni-webhooks": {
"name": "Webhooks",
"description": "Register, list, test, and remove webhook endpoints. Configure event subscriptions (request.completed, provider.error, budget.exceeded, etc.) and manage delivery retries."
"description": "__MISSING__:Register, list, test, and remove webhook endpoints. Configure event subscriptions (request.completed, request.failed, quota.exceeded, etc.) and manage delivery retries."
},
"omni-mcp": {
"name": "MCP Server",

View File

@@ -12202,7 +12202,7 @@
},
"omni-webhooks": {
"name": "وب‌هوک‌ها",
"description": "ثبت، فهرست کردن، آزمایش و حذف نقاط پایانی وب‌هوک. پیکربندی اشتراک‌های رویداد (request.completed، provider.error، budget.exceeded و غیره) و مدیریت تلاش‌های مجدد تحویل."
"description": "__MISSING__:Register, list, test, and remove webhook endpoints. Configure event subscriptions (request.completed, request.failed, quota.exceeded, etc.) and manage delivery retries."
},
"omni-mcp": {
"name": "سرور MCP",

View File

@@ -12202,7 +12202,7 @@
},
"omni-webhooks": {
"name": "Webhookit",
"description": "Rekisteröi, listaa, testaa ja poista webhook-päätepisteitä. Määritä tapahtumatilaukset (request.completed, provider.error, budget.exceeded jne.) ja hallitse toimituksen uudelleenyrityksiä."
"description": "__MISSING__:Register, list, test, and remove webhook endpoints. Configure event subscriptions (request.completed, request.failed, quota.exceeded, etc.) and manage delivery retries."
},
"omni-mcp": {
"name": "MCP-palvelin",

View File

@@ -12202,7 +12202,7 @@
},
"omni-webhooks": {
"name": "Webhooks",
"description": "Enregistrer, lister, tester et supprimer des points de terminaison de webhook. Configurer les abonnements aux événements (request.completed, provider.error, budget.exceeded, etc.) et gérer les tentatives de livraison."
"description": "__MISSING__:Register, list, test, and remove webhook endpoints. Configure event subscriptions (request.completed, request.failed, quota.exceeded, etc.) and manage delivery retries."
},
"omni-mcp": {
"name": "Serveur MCP",

Some files were not shown because too many files have changed in this diff Show More