mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-25 00:22:09 +03:00
Compare commits
35 Commits
fix/codeql
...
fix/better
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
30cac12f5e | ||
|
|
dafb4ae808 | ||
|
|
338c05dc6a | ||
|
|
6945bbaaba | ||
|
|
690f684bfc | ||
|
|
c3cd1f94c0 | ||
|
|
c21460f22a | ||
|
|
9b14896a6c | ||
|
|
29f26293c3 | ||
|
|
cb11592441 | ||
|
|
5ee646e68e | ||
|
|
6984676d95 | ||
|
|
79f8ae9d1e | ||
|
|
04b2c47940 | ||
|
|
24ac71465e | ||
|
|
8d6f91b558 | ||
|
|
c3698eedcb | ||
|
|
adca3b881c | ||
|
|
ac02c5b42f | ||
|
|
07d1816a45 | ||
|
|
3192eb88d5 | ||
|
|
8969526437 | ||
|
|
5518916725 | ||
|
|
3daa455e1f | ||
|
|
d282ec7ad6 | ||
|
|
1e81e521c0 | ||
|
|
a3c3117254 | ||
|
|
d9a883ec53 | ||
|
|
7913447bf0 | ||
|
|
67fba53190 | ||
|
|
9cb91dee74 | ||
|
|
92f58603f9 | ||
|
|
7cec8e32fd | ||
|
|
d137368fb5 | ||
|
|
2904cf849d |
19
.env.example
19
.env.example
@@ -65,6 +65,12 @@ INITIAL_PASSWORD=CHANGEME
|
||||
# OMNIROUTE_RELEASE_REF=origin/main
|
||||
# OMNIROUTE_ALLOW_CANARY_BUILD=1
|
||||
|
||||
# Build-phase signal (#10060). Set to 1 by scripts/build/build-next-isolated.mjs and
|
||||
# inherited by every spawned build worker so the DB layer returns a no-op stub instead
|
||||
# of loading the native better-sqlite3 addon (which aborts the worker on exit).
|
||||
# Never set this for the running server. Used by: src/lib/buildPhase.ts, src/lib/db/core.ts
|
||||
# OMNIROUTE_BUILDING=1
|
||||
|
||||
# Encryption key for SQLite database encryption at rest.
|
||||
# Used by: src/lib/db/encryption.ts — encrypts the entire SQLite database.
|
||||
# Generate: openssl rand -hex 32 | Leave empty to disable DB encryption.
|
||||
@@ -1021,6 +1027,16 @@ PROVIDER_LIMITS_SYNC_SPACING_MS=1500
|
||||
# Used by: open-sse/services/compression/engines/rtk/filterLoader.ts. Default: 0.
|
||||
#OMNIROUTE_RTK_TRUST_PROJECT_FILTERS=0
|
||||
|
||||
# Maximum concurrent synchronous compression workers. Excess jobs wait FIFO.
|
||||
# Used by: open-sse/services/compression/compressionWorkerPool.ts. Default: 2.
|
||||
#OMNI_COMPRESSION_WORKERS=2
|
||||
# Per-job worker timeout (ms). A timed-out worker is terminated and the request fails open.
|
||||
# Used by: open-sse/services/compression/compressionWorkerPool.ts. Default: 120000.
|
||||
#OMNI_COMPRESSION_WORKER_TIMEOUT_MS=120000
|
||||
# Terminate idle compression workers after this many milliseconds.
|
||||
# Used by: open-sse/services/compression/compressionWorkerPool.ts. Default: 60000.
|
||||
#OMNI_COMPRESSION_WORKER_IDLE_MS=60000
|
||||
|
||||
# T02 stacked-pipeline engine circuit-breaker (OPT-IN, default off). When enabled, a compression
|
||||
# engine that throws repeatedly across requests is skipped (fail-open) for a cooldown.
|
||||
# Used by: open-sse/services/compression/pipelineEngineBreaker.ts.
|
||||
@@ -2005,6 +2021,9 @@ APP_LOG_TO_FILE=true
|
||||
# CLIPROXYAPI_HOST=127.0.0.1
|
||||
# CLIPROXYAPI_PORT=5544
|
||||
# CLIPROXYAPI_CONFIG_DIR=~/.cli-proxy-api
|
||||
# Management key for an externally managed instance. Embedded instances use
|
||||
# OmniRoute's encrypted service key.
|
||||
# CLIPROXYAPI_MANAGEMENT_KEY=
|
||||
|
||||
# ── Mux embedded service ──
|
||||
# Override the port where the embedded Mux (coder/mux) agent-orchestration
|
||||
|
||||
4
.github/workflows/opencode-plugin-ci.yml
vendored
4
.github/workflows/opencode-plugin-ci.yml
vendored
@@ -2,11 +2,11 @@ name: opencode-plugin CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, release/v3.8.2]
|
||||
branches: [main, "release/**"]
|
||||
paths:
|
||||
- "@omniroute/opencode-plugin/**"
|
||||
pull_request:
|
||||
branches: [main, release/v3.8.2]
|
||||
branches: [main, "release/**"]
|
||||
paths:
|
||||
- "@omniroute/opencode-plugin/**"
|
||||
types: [opened, synchronize, reopened, ready_for_review]
|
||||
|
||||
@@ -104,7 +104,10 @@ test("models: extracts apiKey from ctx.auth (type=api) and calls fetcher with it
|
||||
// #6859: dynamic-hook catalog keys use the unprefixed omnirouteProviderId
|
||||
// ("omniroute"), not the OC-gate-prefixed hook.id ("opencode-omniroute") —
|
||||
// that prefix must never leak into anything OmniRoute's server parses.
|
||||
assert.ok(out["omniroute/claude-primary"]);
|
||||
// #10345/#10821: bare combo ids (owned_by: "combo") stay unprefixed —
|
||||
// OpenCode looks up `-m <plugin>/<combo>` as model id `<combo>` under the
|
||||
// plugin provider, so `claude-primary` here carries no provider prefix.
|
||||
assert.ok(out["claude-primary"]);
|
||||
});
|
||||
|
||||
test("models: returns {} when ctx.auth is null/undefined/wrong-type/empty-key", async () => {
|
||||
@@ -159,11 +162,15 @@ test("models: maps a sample /v1/models entry to ModelV2 (sanity)", async () => {
|
||||
// omnirouteProviderId ("omniroute") — the OC-gate prefix ("opencode-")
|
||||
// must stay OC-internal (hook.id / AuthHook.provider) and never leak into
|
||||
// anything OmniRoute's own server parses for credential lookup.
|
||||
const claude = out["omniroute/claude-primary"];
|
||||
// #10345/#10821: bare **combo** ids (owned_by: "combo", e.g.
|
||||
// "claude-primary") must also stay unprefixed — OpenCode looks up
|
||||
// `-m <plugin>/<combo>` as model id `<combo>` under the plugin provider.
|
||||
const claude = out["claude-primary"];
|
||||
assert.ok(claude, "claude-primary present");
|
||||
// `mapRawModelToModelV2` stamps the provider prefix on the id so OC's
|
||||
// static-catalog reader resolves `(providerID, modelID)` from the key.
|
||||
assert.equal(claude.id, "omniroute/claude-primary");
|
||||
// `mapRawModelToModelV2` leaves bare combo ids unprefixed (see
|
||||
// src/index.ts mapRawModelToModelV2) so OC's `-m <plugin>/<combo>` lookup
|
||||
// resolves the combo id directly.
|
||||
assert.equal(claude.id, "claude-primary");
|
||||
assert.equal(claude.name, "claude-primary");
|
||||
assert.equal(claude.providerID, "omniroute");
|
||||
assert.equal(claude.api.id, "openai-compatible");
|
||||
|
||||
@@ -180,6 +180,7 @@ _Living section — regenerated 2026-08-12 from all cycle commits (cycle open `e
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- **fix(build):** every route no longer answers HTTP 500 on artifacts built from the release tip ([#11343](https://github.com/diegosouzapw/OmniRoute/issues/11343)) — `next.config.mjs` aliased `better-sqlite3` to its build-time stub **unconditionally**, on the premise that `serverExternalPackages` still won at runtime. It does not: a Turbopack `resolveAlias` rewrites the request *before* the externals check, so the request stopped matching the `better-sqlite3` external entry and the stub was baked into the shipped bundle. The sync driver then failed with `r(...) is not a constructor`, fell through `node:sqlite` and sql.js, and the instrumentation hook aborted at boot. Same failure shape as [#6344](https://github.com/diegosouzapw/OmniRoute/issues/6344), so it gets the same treatment: the alias is opt-in via `OMNIROUTE_BETTER_SQLITE3_STUB=1` through the shared `scripts/build/better-sqlite3-stub-flag.mjs` helper — set it only on a build host that actually hits the SIGABRT build-worker teardown ([#10060](https://github.com/diegosouzapw/OmniRoute/issues/10060)); default builds externalize the real native addon. Regression guards: `tests/unit/better-sqlite3-stub-alias-11343.test.mjs` (5) and the env matrix in `tests/unit/next-config.test.ts`.
|
||||
- **security(search)**: block SSRF via `/v1/search` `provider_options.baseUrl` for the Firecrawl search provider — the client-controlled override is now validated as a public URL before it is used to build the server-side fetch target, so a caller with a valid API key can no longer redirect search requests at loopback, RFC1918, or cloud-metadata hosts — thanks @zmf963
|
||||
- **providers**: honor `PATCH /api/providers/[id]` so `omniroute providers rotate` stops 405ing (the OpenAPI spec and CLI already use PATCH) (PR #10366)
|
||||
- **cli**: route provider test commands through configured connection test endpoints (#10570)
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
isFatalInstrumentationHookFailure,
|
||||
formatAndroidInstrumentationFailureHint,
|
||||
} from "../utils/ensureAndroidCacheDir.mjs";
|
||||
import { resolveServerHost } from "../utils/serverHost.mjs";
|
||||
import { resolveServerHost, resolveExposureWarning } from "../utils/serverHost.mjs";
|
||||
import {
|
||||
resolveMaxOldSpaceMb,
|
||||
calibrateHeapFallbackMb,
|
||||
@@ -162,6 +162,15 @@ export async function runServe(opts = {}) {
|
||||
`);
|
||||
}
|
||||
|
||||
// GHSA-wmgv-ph3p-rv57: the default posture (all interfaces + no API key) is a
|
||||
// deliberate local-first choice, but it must be loud at startup — an operator
|
||||
// on an untrusted network learns the two escape hatches here, not after a
|
||||
// surprise quota bill.
|
||||
const exposureWarning = resolveExposureWarning();
|
||||
if (exposureWarning) {
|
||||
console.warn(`\x1b[33m ⚠ ${exposureWarning}\x1b[0m\n`);
|
||||
}
|
||||
|
||||
const serverWsJs = join(APP_DIR, "server-ws.mjs");
|
||||
const serverJs = existsSync(serverWsJs) ? serverWsJs : join(APP_DIR, "server.js");
|
||||
|
||||
|
||||
@@ -26,7 +26,8 @@
|
||||
"testFailed": "Teste do provedor falhou: {error}",
|
||||
"loginEnabled": "Login: habilitado (senha atualizada)",
|
||||
"loginDisabled": "Login: desabilitado",
|
||||
"providerInfo": "Provedor: {info}"
|
||||
"providerInfo": "Provedor: {info}",
|
||||
"opencode": "Instala e configura o plugin @omniroute/opencode-plugin incluído para o OpenCode"
|
||||
},
|
||||
"doctor": {
|
||||
"title": "OmniRoute Doctor",
|
||||
@@ -254,7 +255,9 @@
|
||||
"no_recovery": "Desabilitar reinício automático em crash (modo debug)",
|
||||
"max_restarts": "Máximo de reinícios em 30s antes de desistir (padrão: 2)",
|
||||
"tray": "Mostrar ícone na bandeja do sistema (apenas desktop, opt-in)",
|
||||
"no_tray": "Desabilitar ícone na bandeja do sistema"
|
||||
"no_tray": "Desabilitar ícone na bandeja do sistema",
|
||||
"tls_cert": "Caminho para um certificado TLS (PEM) para servir HTTPS (também OMNIROUTE_TLS_CERT)",
|
||||
"tls_key": "Caminho para a chave privada TLS (PEM) para servir HTTPS (também OMNIROUTE_TLS_KEY)"
|
||||
},
|
||||
"backup": {
|
||||
"title": "Backup",
|
||||
|
||||
@@ -24,3 +24,34 @@ export function resolveServerHost(
|
||||
}
|
||||
return "0.0.0.0";
|
||||
}
|
||||
|
||||
const LOOPBACK_HOSTS = new Set(["127.0.0.1", "localhost", "::1", "[::1]"]);
|
||||
|
||||
/**
|
||||
* Boot-time exposure warning (GHSA-wmgv-ph3p-rv57): the shipped default binds
|
||||
* all interfaces while the inference plane requires no credentials, so any
|
||||
* LAN peer can spend the operator's quota. That local-first posture is a
|
||||
* deliberate, documented default — but it must be LOUD at startup so an
|
||||
* operator who never read the docs still learns the two escape hatches.
|
||||
*
|
||||
* Returns the warning text when the server will listen on a non-loopback
|
||||
* interface with no API-key requirement, or null when the exposure is closed.
|
||||
*
|
||||
* @param {NodeJS.ProcessEnv} [env]
|
||||
* @param {string} [host]
|
||||
* @returns {string | null}
|
||||
*/
|
||||
export function resolveExposureWarning(env = process.env, host = resolveServerHost(env)) {
|
||||
if (LOOPBACK_HOSTS.has(host)) return null;
|
||||
const requireKey = String(env.REQUIRE_API_KEY || "")
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
if (requireKey === "true" || requireKey === "1" || requireKey === "yes") return null;
|
||||
return (
|
||||
`SECURITY: listening on ${host} with NO API-key requirement — the inference ` +
|
||||
`plane (/v1/*) is reachable by ANY device that can route to this host, and ` +
|
||||
`requests are billed to your configured providers. This local-first default ` +
|
||||
`is intentional, but on an untrusted network either set REQUIRE_API_KEY=true ` +
|
||||
`or bind loopback with OMNIROUTE_SERVER_HOST=127.0.0.1.`
|
||||
);
|
||||
}
|
||||
|
||||
1
changelog.d/features/10556-elevenlabs-native-routes.md
Normal file
1
changelog.d/features/10556-elevenlabs-native-routes.md
Normal file
@@ -0,0 +1 @@
|
||||
- **feat(audio):** proxy native ElevenLabs voices, text-to-speech, and speech-to-text HTTP routes through stored OmniRoute credentials, preserving query strings, multipart uploads, binary responses, and upstream errors (#10556).
|
||||
1
changelog.d/features/10590-google-ai-studio-tts.md
Normal file
1
changelog.d/features/10590-google-ai-studio-tts.md
Normal file
@@ -0,0 +1 @@
|
||||
- Added Google AI Studio Gemini batch text-to-speech support through `POST /v1/audio/speech`.
|
||||
3
changelog.d/features/11023-compression-worker-pool.md
Normal file
3
changelog.d/features/11023-compression-worker-pool.md
Normal file
@@ -0,0 +1,3 @@
|
||||
- Run synchronous RTK and Caveman request compression in a bounded worker-thread pool, keeping
|
||||
large `/v1/responses` compression heaps outside the HTTP isolate while preserving strict
|
||||
fail-open behavior and per-engine telemetry.
|
||||
@@ -0,0 +1 @@
|
||||
- **feat(dashboard):** lead Traffic Inspector with a purpose-first header that separates "what happened" from "how it happened", so beginners can read request outcomes without drowning in protocol detail ([#11283](https://github.com/diegosouzapw/OmniRoute/pull/11283))
|
||||
1
changelog.d/features/11286-essentials-sidebar-preset.md
Normal file
1
changelog.d/features/11286-essentials-sidebar-preset.md
Normal file
@@ -0,0 +1 @@
|
||||
- **feat(dashboard):** add an Essentials sidebar preset that shows only the beginner core path (Home → Endpoints → API Keys → Providers → Health → Settings) while keeping Advanced tools reachable via Command Palette search ([#11286](https://github.com/diegosouzapw/OmniRoute/pull/11286))
|
||||
1
changelog.d/features/6342-cliproxy-account-health.md
Normal file
1
changelog.d/features/6342-cliproxy-account-health.md
Normal file
@@ -0,0 +1 @@
|
||||
- feat(services): show sanitized CLIProxyAPI account health from its authenticated management API without exposing credentials, file paths, or raw account metadata (#6342)
|
||||
1
changelog.d/fixes/10352-github-access-token-health.md
Normal file
1
changelog.d/fixes/10352-github-access-token-health.md
Normal file
@@ -0,0 +1 @@
|
||||
- **fix(github):** proactive credential health now verifies GitHub access tokens through the existing Copilot token exchange, marks only a confirmed `401 Unauthorized` as expired, and leaves rate limits, permission failures, upstream failures, and network errors routable ([#10352](https://github.com/diegosouzapw/OmniRoute/issues/10352)) — thanks @RaviTharuma
|
||||
1
changelog.d/fixes/10815-kiro-social-multi-account.md
Normal file
1
changelog.d/fixes/10815-kiro-social-multi-account.md
Normal file
@@ -0,0 +1 @@
|
||||
- fix(oauth): stop treating the Kiro profile ARN as an account identity in `findKiroConnectionByIdentity()`, so a second Google/GitHub social login creates a new connection instead of overwriting the first — distinct Builder ID accounts share the same CodeWhisperer profile ARN, and the social token is not a JWT, so no e-mail was available to disambiguate them (#10815)
|
||||
1
changelog.d/fixes/10851-openapi-spec-auth-contract.md
Normal file
1
changelog.d/fixes/10851-openapi-spec-auth-contract.md
Normal file
@@ -0,0 +1 @@
|
||||
- Document the conditional management authentication and 401/403 responses for `GET /api/openapi/spec`.
|
||||
1
changelog.d/fixes/11297-opencode-subagent-sessionid.md
Normal file
1
changelog.d/fixes/11297-opencode-subagent-sessionid.md
Normal file
@@ -0,0 +1 @@
|
||||
- **fix(translator):** preserve omitted OpenCode `subagent.sessionID` values — optional default-less plain strings now use the Responses `null = omit` sentinel and are stripped before the client sees the tool call, so Codex/Responses no longer invent filler session IDs ([#11297](https://github.com/diegosouzapw/OmniRoute/pull/11297)) — thanks @ofonseca-pyming
|
||||
@@ -0,0 +1 @@
|
||||
- **fix(db):** group model patterns escape regex metacharacters, so `gpt-4.1*` no longer matches `gpt-4o1-preview` and a pattern like `gpt-4(*` no longer throws `SyntaxError` out of the completion and `/v1/models` paths ([#11311](https://github.com/diegosouzapw/OmniRoute/pull/11311))
|
||||
1
changelog.d/fixes/11319-upstream-proxy-host-spelling.md
Normal file
1
changelog.d/fixes/11319-upstream-proxy-host-spelling.md
Normal file
@@ -0,0 +1 @@
|
||||
- **fix(db):** the upstream proxy URL check judges the host by address instead of by spelling, so `http://[::ffff:169.254.169.254]`, `[::ffff:10.0.0.5]`, ULA/link-local and CGNAT targets are refused like their dotted equivalents ([#11319](https://github.com/diegosouzapw/OmniRoute/pull/11319))
|
||||
1
changelog.d/fixes/11325-i18n-pt-placeholder-parity.md
Normal file
1
changelog.d/fixes/11325-i18n-pt-placeholder-parity.md
Normal file
@@ -0,0 +1 @@
|
||||
- **fix(i18n):** three `pt` strings had dropped their placeholders — the cache tile's subtitle repeated its own label instead of showing `{total}` — and a unit test now enforces placeholder parity with `en` across all locales ([#11325](https://github.com/diegosouzapw/OmniRoute/pull/11325))
|
||||
1
changelog.d/fixes/11326-kie-market-google-imagen-ids.md
Normal file
1
changelog.d/fixes/11326-kie-market-google-imagen-ids.md
Normal file
@@ -0,0 +1 @@
|
||||
- **fix(kie):** map the remaining `google-imagen/*` KIE Market catalog ids (`nano-banana`, `nano-banana-pro`, `nano-banana-edit`) to their real, KIE-documented upstream `model` values — `#11225`'s fix only covered `nano-banana-2` ([#11326](https://github.com/diegosouzapw/OmniRoute/pull/11326)).
|
||||
1
changelog.d/fixes/11328-upstream-headers-proxy-auth.md
Normal file
1
changelog.d/fixes/11328-upstream-headers-proxy-auth.md
Normal file
@@ -0,0 +1 @@
|
||||
- **fix(security):** `proxy-authorization` and `proxy-authenticate` are refused as upstream/custom headers, so a proxy credential is no longer forwarded to the model provider — the canonical denylist now matches the RFC 7230 §6.1 set the rest of the codebase already strips ([#11328](https://github.com/diegosouzapw/OmniRoute/pull/11328))
|
||||
1
changelog.d/maintenance/11018-database-cache-docs.md
Normal file
1
changelog.d/maintenance/11018-database-cache-docs.md
Normal file
@@ -0,0 +1 @@
|
||||
- **docs(database):** align the SQLite cache guide with the 65,536 KiB runtime default, supported 1–1,000,000 KiB range, and live Settings application behavior ([#11018](https://github.com/diegosouzapw/OmniRoute/issues/11018))
|
||||
@@ -0,0 +1,3 @@
|
||||
- **fix(deps):** prevent pnpm from auto-installing the unused `@lobehub/ui` peer subtree of
|
||||
`@lobehub/icons`, keeping six unneeded packages with incompatible or unverifiable license
|
||||
metadata out of production installs ([#11342](https://github.com/diegosouzapw/OmniRoute/pull/11342)).
|
||||
@@ -1,5 +1,9 @@
|
||||
{
|
||||
"_comment": "Allowlist anti-slopsquatting (check-deps.mjs). Toda dep nova exige adicao EXPLICITA aqui apos verificar que e legitima.",
|
||||
"_justifications": {
|
||||
"@testing-library/dom": "Peer dep obrigatoria de @testing-library/react v16 (adicionada no PR #11224); Refs #9985.",
|
||||
"@testing-library/user-event": "Utilitario oficial do ecossistema testing-library para testes de UI (adicionada no PR #11224); Refs #9985."
|
||||
},
|
||||
"allowed": [
|
||||
"@atjsh/llmlingua-2",
|
||||
"@aws-sdk/client-bedrock-runtime",
|
||||
@@ -20,8 +24,10 @@
|
||||
"@stryker-mutator/tap-runner",
|
||||
"@swc/helpers",
|
||||
"@tailwindcss/postcss",
|
||||
"@testing-library/dom",
|
||||
"@testing-library/jest-dom",
|
||||
"@testing-library/react",
|
||||
"@testing-library/user-event",
|
||||
"@toon-format/toon",
|
||||
"@types/better-sqlite3",
|
||||
"@types/bun",
|
||||
|
||||
@@ -3445,7 +3445,7 @@
|
||||
},
|
||||
"tests/integration/qdrant-routes.test.ts": {
|
||||
"@typescript-eslint/no-explicit-any": {
|
||||
"count": 19
|
||||
"count": 3
|
||||
}
|
||||
},
|
||||
"tests/integration/quota-pools-usage.test.ts": {
|
||||
@@ -4029,10 +4029,10 @@
|
||||
},
|
||||
"tests/unit/cli-combo-suggest-commands.test.ts": {
|
||||
"@typescript-eslint/no-explicit-any": {
|
||||
"count": 16
|
||||
"count": 14
|
||||
},
|
||||
"@typescript-eslint/no-unused-vars": {
|
||||
"count": 2
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"tests/unit/cli-completion-dynamic.test.ts": {
|
||||
@@ -4042,7 +4042,7 @@
|
||||
},
|
||||
"tests/unit/cli-compression-commands.test.ts": {
|
||||
"@typescript-eslint/no-explicit-any": {
|
||||
"count": 32
|
||||
"count": 20
|
||||
}
|
||||
},
|
||||
"tests/unit/cli-context-eng-commands.test.ts": {
|
||||
@@ -4099,7 +4099,7 @@
|
||||
},
|
||||
"tests/unit/cli-mcp-call-commands.test.ts": {
|
||||
"@typescript-eslint/no-explicit-any": {
|
||||
"count": 16
|
||||
"count": 10
|
||||
}
|
||||
},
|
||||
"tests/unit/cli-memory-commands.test.ts": {
|
||||
@@ -4130,7 +4130,7 @@
|
||||
},
|
||||
"tests/unit/cli-oneproxy-commands.test.ts": {
|
||||
"@typescript-eslint/no-explicit-any": {
|
||||
"count": 22
|
||||
"count": 14
|
||||
},
|
||||
"@typescript-eslint/no-unused-vars": {
|
||||
"count": 1
|
||||
@@ -4203,9 +4203,6 @@
|
||||
"tests/unit/cli-resilience-commands.test.ts": {
|
||||
"@typescript-eslint/no-explicit-any": {
|
||||
"count": 16
|
||||
},
|
||||
"@typescript-eslint/no-unused-vars": {
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"tests/unit/cli-runtime-extended.test.ts": {
|
||||
@@ -4238,7 +4235,7 @@
|
||||
},
|
||||
"tests/unit/cli-skills-commands.test.ts": {
|
||||
"@typescript-eslint/no-explicit-any": {
|
||||
"count": 22
|
||||
"count": 16
|
||||
}
|
||||
},
|
||||
"tests/unit/cli-stop-supervisor-respawn-9455.test.ts": {
|
||||
@@ -6620,4 +6617,4 @@
|
||||
"count": 2
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6866,7 +6866,11 @@ paths:
|
||||
Returns a structured JSON catalog parsed from this `openapi.yaml`,
|
||||
including info, servers, tags, schemas, and a flat list of endpoints
|
||||
(method, path, tags, summary, security, parameters, responses).
|
||||
Used by the in-app API explorer.
|
||||
Used by the in-app API explorer. When `requireLogin` is enabled, this
|
||||
management endpoint requires an authenticated dashboard session;
|
||||
otherwise it is available without authentication.
|
||||
security:
|
||||
- ManagementSessionAuth: []
|
||||
responses:
|
||||
"200":
|
||||
description: Parsed OpenAPI catalog
|
||||
@@ -6920,6 +6924,10 @@ paths:
|
||||
type: string
|
||||
"404":
|
||||
description: openapi.yaml file not found on disk
|
||||
"401":
|
||||
$ref: "#/components/responses/ManagementAuthenticationRequired"
|
||||
"403":
|
||||
$ref: "#/components/responses/ManagementInvalidToken"
|
||||
"500":
|
||||
description: Failed to parse OpenAPI spec
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
title: "Database Schema & Operations Guide"
|
||||
version: 3.8.40
|
||||
lastUpdated: 2026-06-28
|
||||
version: 3.8.50
|
||||
lastUpdated: 2026-08-23
|
||||
---
|
||||
|
||||
# Database Schema & Operations Guide
|
||||
@@ -43,12 +43,17 @@ For **single-user, single-instance** deployments (the primary OmniRoute use case
|
||||
db.pragma("journal_mode = WAL");
|
||||
db.pragma("busy_timeout = 2000");
|
||||
db.pragma("synchronous = NORMAL");
|
||||
// Settings > System & Storage > Cache Size is applied as KiB.
|
||||
db.pragma("cache_size = -16384");
|
||||
db.pragma(`cache_size = -${DEFAULT_DATABASE_SETTINGS.optimization.cacheSize}`);
|
||||
```
|
||||
|
||||
WAL allows **concurrent reads** during writes — important for the dashboard, which queries while requests are being recorded.
|
||||
|
||||
The default cache size is **65,536 KiB (64 MiB)**. SQLite interprets a negative
|
||||
`cache_size` as an approximate upper bound in KiB and allocates pages on demand.
|
||||
**Settings > System & Storage > Cache Size** accepts integer values from **1 to
|
||||
1,000,000 KiB**; saving the setting applies it to the live database connection,
|
||||
and OmniRoute restores the persisted value at startup.
|
||||
|
||||
---
|
||||
|
||||
## Database Location
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
---
|
||||
title: "CLI Tools — OmniRoute"
|
||||
version: 3.8.50
|
||||
lastUpdated: 2026-08-18
|
||||
lastUpdated: 2026-08-23
|
||||
---
|
||||
|
||||
# CLI Tools — OmniRoute
|
||||
|
||||
Last updated: 2026-08-18
|
||||
Last updated: 2026-08-23
|
||||
|
||||
OmniRoute integrates with three categories of CLI tools spread across three dedicated dashboard pages:
|
||||
|
||||
| Page | Route | Concept | Count |
|
||||
| -------------- | ----------------------- | ------------------------------------------------------------------------- | ------------ |
|
||||
| **CLI Code's** | `/dashboard/cli-code` | Coding tools you point at OmniRoute (Client → CLI → OmniRoute → Provider) | 26 |
|
||||
| **CLI Agents** | `/dashboard/cli-agents` | Autonomous agents you point at OmniRoute (same flow, broader scope) | 8 |
|
||||
| **CLI Agents** | `/dashboard/cli-agents` | Autonomous agents you point at OmniRoute (same flow, broader scope) | 9 |
|
||||
| **ACP Agents** | `/dashboard/acp-agents` | CLIs that OmniRoute spawns as backend via stdio/ACP (reverse flow) | see registry |
|
||||
|
||||
Legacy routes redirect via 308: `/dashboard/cli-tools` → `/dashboard/cli-code`, `/dashboard/agents` → `/dashboard/acp-agents`.
|
||||
|
||||
@@ -88,6 +88,7 @@ OmniRoute uses **SQLite** (via `better-sqlite3`) for all persistence. These vari
|
||||
| `OMNIROUTE_RELEASE_REF` | `origin/main` | `scripts/build/buildProvenance.ts` | Ref the pack-artifact provenance gate checks the build SHA against (#10427). |
|
||||
| `OMNIROUTE_ALLOW_CANARY_BUILD` | _(unset)_ | `scripts/build/buildProvenance.ts` | Set to `1` to allow packing a build whose SHA is not on the release line, recording it as a deliberate canary instead of failing the gate (#10427). |
|
||||
| `OMNIROUTE_SMOKE_API_KEY` | _(unset)_ | `scripts/ops/deploy-canary.mjs` | API key for the canary-deploy smoke probe, sent as `Authorization: Bearer` on `/v1/chat/completions`. Only used by the deploy script (#10429), never by the server. Not related to the `OMNIROUTE_SMOKE_*` variables of the opt-in CLI smoke harness (`RUN_CLI_SMOKE=1`, `OMNIROUTE_SMOKE_BASE_URL/MODEL/API_KEY_ENV/TARGETS/TIMEOUT_MS` in `tests/integration/upstream-cli-smoke.int.test.ts`) — see [CLI Integrations → Real smoke sweep](../guides/CLI-INTEGRATIONS.md). |
|
||||
| `OMNIROUTE_BUILDING` | _(unset)_ | `src/lib/buildPhase.ts` | Build-phase signal (#10060): set to `1` by `scripts/build/build-next-isolated.mjs` and inherited by every spawned build worker so the DB layer returns a no-op stub instead of loading the native better-sqlite3 addon (which aborts the worker on exit). Never set for the running server. |
|
||||
| `OMNIROUTE_DATA_DIR` | _(unset)_ | `open-sse/executors/promptql/threadSticky.ts` | **Fallback alias** for `DATA_DIR`, checked only when `DATA_DIR` is unset. Used to locate the PromptQL executor's on-disk thread-sticky session cache (`<dir>/promptql-thread-sessions.json`); if neither var is set, the cache stays in-memory only (not persisted across restarts). |
|
||||
| `STORAGE_ENCRYPTION_KEY` | _(empty = disabled)_ | `src/lib/db/encryption.ts` | AES key for full SQLite database encryption at rest. Generate with `openssl rand -hex 32`. |
|
||||
| `STORAGE_ENCRYPTION_KEY_VERSION` | `v1` | `scripts/build/bootstrap-env.mjs`, `electron/main.js` | Version label for the encryption key. Increment when performing key rotation to support decryption of old backups. |
|
||||
@@ -530,6 +531,9 @@ detection above).
|
||||
| `OMNIROUTE_CONFIG_HOT_RELOAD_MS` | `5000` | `src/lib/config/hotReload.ts` | Polling interval (ms) for config hot-reload. Lower than `1000` is rejected. |
|
||||
| `OMNIROUTE_DISABLE_REDIS_AUTH_CACHE` | _(enabled)_ | `src/lib/db/apiKeys.ts` | Set `1` to bypass the Redis-backed API-key auth cache (forces DB reads). |
|
||||
| `OMNIROUTE_RTK_TRUST_PROJECT_FILTERS` | `0` | `open-sse/services/compression/engines/rtk/filterLoader.ts` | Trust user-managed RTK project filter rules without strict signature checks. |
|
||||
| `OMNI_COMPRESSION_WORKERS` | `2` | `open-sse/services/compression/compressionWorkerPool.ts` | Maximum concurrent synchronous RTK/Caveman workers; excess jobs wait FIFO. |
|
||||
| `OMNI_COMPRESSION_WORKER_TIMEOUT_MS` | `120000` | `open-sse/services/compression/compressionWorkerPool.ts` | Per-job timeout in milliseconds. Timed-out workers are terminated and the request fails open unchanged. |
|
||||
| `OMNI_COMPRESSION_WORKER_IDLE_MS` | `60000` | `open-sse/services/compression/compressionWorkerPool.ts` | Idle lifetime in milliseconds before an unused compression worker is terminated. |
|
||||
| `COMPRESSION_PIPELINE_BREAKER_ENABLED` | `false` | `open-sse/services/compression/pipelineEngineBreaker.ts` | T02 stacked-pipeline per-engine circuit-breaker master switch. **Opt-in (default off)** — when on, an engine that throws repeatedly across requests is skipped (fail-open) for a cooldown; off = byte-identical legacy behavior. |
|
||||
| `COMPRESSION_PIPELINE_BREAKER_THRESHOLD` | `3` | `open-sse/services/compression/pipelineEngineBreaker.ts` | Consecutive cross-request failures before an engine's breaker opens. |
|
||||
| `COMPRESSION_PIPELINE_BREAKER_COOLDOWN_MS` | `30000` | `open-sse/services/compression/pipelineEngineBreaker.ts` | Milliseconds an opened engine stays skipped before a half-open probe. |
|
||||
@@ -1040,6 +1044,7 @@ desktop install.
|
||||
| `EMBED_WS_PROXY_PORT` | `20131` | `src/lib/services/embedWsProxy.ts` | Port for the embedded-service WebSocket proxy server. |
|
||||
| `CLIPROXYAPI_HOST` | `127.0.0.1` | `open-sse/executors/cliproxyapi.ts` | CLIProxyAPI bridge host (legacy integration). |
|
||||
| `CLIPROXYAPI_PORT` | `5544` | `open-sse/executors/cliproxyapi.ts` | CLIProxyAPI bridge port. |
|
||||
| `CLIPROXYAPI_MANAGEMENT_KEY` | _(empty)_ | `src/lib/services/cliproxyAccountHealth.ts` | Management key for account-health reads from an externally managed CLIProxyAPI instance. |
|
||||
| `CLIPROXYAPI_CONFIG_DIR` | `~/.cli-proxy-api` | `src/lib/versionManager/processManager.ts` | CLIProxyAPI config directory. |
|
||||
| `MUX_SERVICE_PORT` | `8322` | `src/lib/services/bootstrap.ts` | Override the port where the embedded Mux (coder/mux) agent-orchestration daemon listens (always 127.0.0.1). |
|
||||
| `DARIO_HOST` | `127.0.0.1` | `open-sse/executors/dario.ts` | Dario embedded-service bind/connect host (loopback only by default). |
|
||||
|
||||
@@ -2,6 +2,7 @@ import createNextIntlPlugin from "next-intl/plugin";
|
||||
import { createMDX } from "fumadocs-mdx/next";
|
||||
import { dirname } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { betterSqlite3AliasFor } from "./scripts/build/better-sqlite3-stub-flag.mjs";
|
||||
import { mitmManagerAliasFor } from "./scripts/build/mitm-stub-flag.mjs";
|
||||
import { normalizeBasePath } from "./scripts/build/normalizeBasePath.mjs";
|
||||
import {
|
||||
@@ -138,10 +139,14 @@ const nextConfig = {
|
||||
// the stub to every npm/Electron/VPS artifact and broke Agent Bridge
|
||||
// start for all non-Docker users (#6344). See scripts/build/mitm-stub-flag.mjs.
|
||||
...mitmManagerAliasFor(process.env),
|
||||
// Build-time stub so the bundler never traces the native better-sqlite3
|
||||
// addon into a build worker (SIGABRT at worker teardown). Runtime still
|
||||
// uses the real package via serverExternalPackages. (#10060)
|
||||
"better-sqlite3": "./src/lib/db/better-sqlite3.stub.js",
|
||||
// better-sqlite3 → build-time stub ONLY where the build worker actually
|
||||
// aborts while tracing the native addon (SIGABRT at worker teardown,
|
||||
// #10060); opt in with OMNIROUTE_BETTER_SQLITE3_STUB=1. The alias used to
|
||||
// be unconditional on the premise that serverExternalPackages still won
|
||||
// at runtime — it does not: resolveAlias rewrites the request before the
|
||||
// externals check, so the stub was bundled and EVERY route answered 500
|
||||
// (#11343). See scripts/build/better-sqlite3-stub-flag.mjs.
|
||||
...betterSqlite3AliasFor(process.env),
|
||||
...minimalBuildAliases,
|
||||
},
|
||||
// src/lib/agentSkills/generator.ts builds its fs base path from a runtime
|
||||
|
||||
@@ -287,6 +287,19 @@ export const AUDIO_TRANSLATION_PROVIDERS: Record<string, AudioProvider> = {
|
||||
};
|
||||
|
||||
export const AUDIO_SPEECH_PROVIDERS: Record<string, AudioProvider> = {
|
||||
google: {
|
||||
id: "google",
|
||||
credentialProviderId: "gemini",
|
||||
baseUrl: "https://generativelanguage.googleapis.com/v1beta/models",
|
||||
authType: "apikey",
|
||||
authHeader: "x-goog-api-key",
|
||||
format: "gemini-tts",
|
||||
models: [
|
||||
{ id: "gemini-3.1-flash-tts-preview", name: "Gemini 3.1 Flash TTS" },
|
||||
{ id: "gemini-2.5-flash-preview-tts", name: "Gemini 2.5 Flash TTS" },
|
||||
{ id: "gemini-2.5-pro-preview-tts", name: "Gemini 2.5 Pro TTS" },
|
||||
],
|
||||
},
|
||||
vertex: {
|
||||
id: "vertex",
|
||||
baseUrl: "https://us-central1-aiplatform.googleapis.com/v1",
|
||||
|
||||
@@ -181,6 +181,29 @@ export function getRegistryEntry(provider: string): RegistryEntry | null {
|
||||
return REGISTRY[provider] || _byAlias.get(provider) || null;
|
||||
}
|
||||
|
||||
/** Resolve only a model's explicit reasoning vocabulary. */
|
||||
export function getRegistryModelThinkingEfforts(
|
||||
provider: string,
|
||||
modelId: string
|
||||
): readonly string[] | undefined {
|
||||
const entry = getRegistryEntry(provider);
|
||||
if (!entry) return undefined;
|
||||
const model = entry.models.find((candidate) => candidate.id === modelId);
|
||||
return model?.supportedThinkingEfforts;
|
||||
}
|
||||
|
||||
/** Resolve a model's explicit reasoning vocabulary before its provider fallback. */
|
||||
export function getRegistryThinkingEfforts(
|
||||
provider: string,
|
||||
modelId: string
|
||||
): readonly string[] | undefined {
|
||||
const entry = getRegistryEntry(provider);
|
||||
if (!entry) return undefined;
|
||||
const modelEfforts = getRegistryModelThinkingEfforts(provider, modelId);
|
||||
if (modelEfforts !== undefined) return modelEfforts;
|
||||
return entry.defaultSupportedThinkingEfforts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decide whether a non-empty live catalog may exclude omitted static models
|
||||
* during request routing and wildcard expansion.
|
||||
|
||||
@@ -9,6 +9,7 @@ export const ollama_cloudProvider: RegistryEntry = {
|
||||
modelsUrl: "https://ollama.com/api/tags",
|
||||
authType: "apikey",
|
||||
authHeader: "bearer",
|
||||
defaultSupportedThinkingEfforts: ["none", "low", "medium", "high", "max"],
|
||||
// Note: rate limits vary by plan (free = "Light usage", Pro = more, Max = 5x Pro).
|
||||
// Users can generate API keys at https://ollama.com/settings/keys
|
||||
models: [
|
||||
@@ -24,23 +25,20 @@ export const ollama_cloudProvider: RegistryEntry = {
|
||||
supportsReasoning: true,
|
||||
supportedThinkingEfforts: ["low", "medium", "high"],
|
||||
},
|
||||
// #10788: Ollama Cloud accepts low|medium|high|max|none uniformly across
|
||||
// its reasoning-capable models (see supportsMaxEffortForProvider's
|
||||
// isOllamaCloud comment in open-sse/executors/base/reasoningEffort.ts) —
|
||||
// declare supportedThinkingEfforts so appendSyncedEffortVariants() (which
|
||||
// runs before static-model capability enrichment) can synthesize the
|
||||
// catalog's selectable -low/-high/-max variant ids for these models.
|
||||
// #10788: these models accept none|low|medium|high|max. Keep their explicit
|
||||
// declarations aligned with the provider fallback so the static and synced
|
||||
// catalog paths expose the same native vocabulary.
|
||||
{
|
||||
id: "deepseek-v4-pro",
|
||||
name: "DeepSeek V4 Pro",
|
||||
supportsReasoning: true,
|
||||
supportedThinkingEfforts: ["low", "medium", "high", "max"],
|
||||
supportedThinkingEfforts: ["none", "low", "medium", "high", "max"],
|
||||
},
|
||||
{
|
||||
id: "deepseek-v4-flash",
|
||||
name: "DeepSeek V4 Flash",
|
||||
supportsReasoning: true,
|
||||
supportedThinkingEfforts: ["low", "medium", "high", "max"],
|
||||
supportedThinkingEfforts: ["none", "low", "medium", "high", "max"],
|
||||
},
|
||||
{ id: "kimi-k2.6", name: "Kimi K2.6" },
|
||||
// Ollama Cloud accepts low|medium|high|max|none and rejects xhigh, so the
|
||||
@@ -50,14 +48,14 @@ export const ollama_cloudProvider: RegistryEntry = {
|
||||
name: "GLM 5.1",
|
||||
supportsReasoning: true,
|
||||
supportsXHighEffort: false,
|
||||
supportedThinkingEfforts: ["low", "medium", "high", "max"],
|
||||
supportedThinkingEfforts: ["none", "low", "medium", "high", "max"],
|
||||
},
|
||||
{
|
||||
id: "glm-5.2",
|
||||
name: "GLM 5.2",
|
||||
supportsReasoning: true,
|
||||
supportsXHighEffort: false,
|
||||
supportedThinkingEfforts: ["low", "medium", "high", "max"],
|
||||
supportedThinkingEfforts: ["none", "low", "medium", "high", "max"],
|
||||
},
|
||||
// #3110: MiniMax M3 via Ollama
|
||||
{ id: "minimax-m3", name: "MiniMax M3", contextLength: 1048576, supportsVision: true },
|
||||
|
||||
@@ -139,6 +139,9 @@ export interface RegistryEntry {
|
||||
requestDefaults?: ProviderRequestDefaults;
|
||||
oauth?: RegistryOAuth;
|
||||
models: RegistryModel[];
|
||||
/** Provider-native reasoning vocabulary for reasoning-capable passthrough models
|
||||
* that do not have an explicit per-model declaration. */
|
||||
defaultSupportedThinkingEfforts?: readonly string[];
|
||||
modelsUrl?: string;
|
||||
/** Prefix to prepend to model IDs before upstream API calls (e.g. "accounts/fireworks/models/") */
|
||||
modelIdPrefix?: string;
|
||||
|
||||
81
open-sse/executors/geminiTts.ts
Normal file
81
open-sse/executors/geminiTts.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import { Buffer } from "node:buffer";
|
||||
import { extractInlineAudio, parsePcmSampleRate, pcmToWav } from "./vertexMedia.ts";
|
||||
import { CORS_HEADERS } from "../utils/cors.ts";
|
||||
import { upstreamErrorResponse } from "../utils/audioResponse.ts";
|
||||
import { errorResponse } from "../utils/error.ts";
|
||||
|
||||
type GeminiTtsCredentials = {
|
||||
apiKey?: string | null;
|
||||
accessToken?: string | null;
|
||||
};
|
||||
|
||||
export class GeminiTtsUpstreamError extends Error {
|
||||
constructor(
|
||||
public readonly response: Response,
|
||||
public readonly body: string
|
||||
) {
|
||||
super(`Gemini TTS upstream error (${response.status})`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function geminiGenerateSpeech(
|
||||
credentials: GeminiTtsCredentials,
|
||||
options: { model: string; text: string; voice: string }
|
||||
): Promise<Buffer> {
|
||||
const headers: Record<string, string> = { "Content-Type": "application/json" };
|
||||
if (credentials.apiKey) {
|
||||
headers["x-goog-api-key"] = credentials.apiKey;
|
||||
} else if (credentials.accessToken) {
|
||||
headers.Authorization = `Bearer ${credentials.accessToken}`;
|
||||
}
|
||||
|
||||
const response = await fetch(
|
||||
`https://generativelanguage.googleapis.com/v1beta/models/${encodeURIComponent(options.model)}:generateContent`,
|
||||
{
|
||||
method: "POST",
|
||||
headers,
|
||||
body: JSON.stringify({
|
||||
contents: [{ parts: [{ text: options.text }] }],
|
||||
generationConfig: {
|
||||
responseModalities: ["AUDIO"],
|
||||
speechConfig: {
|
||||
voiceConfig: {
|
||||
prebuiltVoiceConfig: { voiceName: options.voice },
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
}
|
||||
);
|
||||
if (!response.ok) {
|
||||
throw new GeminiTtsUpstreamError(response, await response.text());
|
||||
}
|
||||
|
||||
const inline = extractInlineAudio(await response.json());
|
||||
if (!inline) throw new Error("Gemini TTS response did not contain audio data");
|
||||
return pcmToWav(Buffer.from(inline.base64, "base64"), parsePcmSampleRate(inline.mimeType));
|
||||
}
|
||||
|
||||
export async function handleGeminiTtsSpeech(
|
||||
credentials: GeminiTtsCredentials,
|
||||
options: { model: string; text: string; voice?: unknown }
|
||||
): Promise<Response> {
|
||||
try {
|
||||
const wav = await geminiGenerateSpeech(credentials, {
|
||||
model: options.model,
|
||||
text: options.text,
|
||||
voice:
|
||||
typeof options.voice === "string" && options.voice.trim() ? options.voice.trim() : "Kore",
|
||||
});
|
||||
return new Response(new Uint8Array(wav), {
|
||||
status: 200,
|
||||
headers: { ...CORS_HEADERS, "Content-Type": "audio/wav" },
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof GeminiTtsUpstreamError) {
|
||||
return upstreamErrorResponse(error.response, error.body);
|
||||
}
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
return errorResponse(500, `Speech request failed: ${message}`);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
import { randomBytes } from "node:crypto";
|
||||
|
||||
import {
|
||||
BaseExecutor,
|
||||
ExecuteInput,
|
||||
@@ -13,6 +15,11 @@ import {
|
||||
import { sanitizeResponsesInputItems } from "../services/responsesInputSanitizer.ts";
|
||||
import { stripUnsupportedParams } from "../translator/paramSupport.ts";
|
||||
|
||||
/** Correlation-id fallback for runtimes without crypto.randomUUID — still CSPRNG-backed. */
|
||||
function randomIdFallback(): string {
|
||||
return `${Date.now()}-${randomBytes(9).toString("hex")}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* What a Copilot credential refresh resolves to.
|
||||
*
|
||||
@@ -329,7 +336,7 @@ export class GithubExecutor extends BaseExecutor {
|
||||
...getGitHubCopilotChatHeaders(stream ? "text/event-stream" : "application/json", initiator),
|
||||
Authorization: `Bearer ${token}`,
|
||||
"x-request-id":
|
||||
crypto.randomUUID?.() || `${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
||||
crypto.randomUUID?.() || randomIdFallback(),
|
||||
};
|
||||
|
||||
// Per-call / per-conversation / per-turn correlation ids the @github/copilot
|
||||
@@ -338,7 +345,7 @@ export class GithubExecutor extends BaseExecutor {
|
||||
// fresh uuids. A Copilot-aware client may pin the session/task ids across a
|
||||
// conversation via its own headers — honor those when present, else mint.
|
||||
const genId = () =>
|
||||
crypto.randomUUID?.() || `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
||||
crypto.randomUUID?.() || randomIdFallback();
|
||||
headers["x-interaction-id"] = this.readClientHeader(clientHeaders, "x-interaction-id") || genId();
|
||||
headers["x-client-session-id"] =
|
||||
this.readClientHeader(clientHeaders, "x-client-session-id") || genId();
|
||||
|
||||
@@ -156,13 +156,13 @@ export function pcmToWav(
|
||||
return Buffer.concat([header, pcm]);
|
||||
}
|
||||
|
||||
function parseSampleRate(mimeType: string | undefined): number {
|
||||
export function parsePcmSampleRate(mimeType: string | undefined): number {
|
||||
if (!mimeType) return 24000;
|
||||
const match = /rate=(\d+)/i.exec(mimeType);
|
||||
return match ? parseInt(match[1], 10) : 24000;
|
||||
}
|
||||
|
||||
function extractInlineAudio(
|
||||
export function extractInlineAudio(
|
||||
data: unknown
|
||||
): { base64: string; mimeType: string } | null {
|
||||
const parts = (data as { candidates?: Array<{ content?: { parts?: unknown[] } }> })?.candidates?.[0]
|
||||
@@ -215,7 +215,7 @@ export async function vertexGenerateSpeech(
|
||||
const inline = extractInlineAudio(data);
|
||||
if (!inline) throw new Error("Vertex TTS returned no audio content");
|
||||
const pcm = Buffer.from(inline.base64, "base64");
|
||||
return { audio: pcmToWav(pcm, parseSampleRate(inline.mimeType)), contentType: "audio/wav" };
|
||||
return { audio: pcmToWav(pcm, parsePcmSampleRate(inline.mimeType)), contentType: "audio/wav" };
|
||||
}
|
||||
|
||||
/** Gemini transcription (audio → text). `audioBase64` is the raw file bytes, base64-encoded. */
|
||||
|
||||
@@ -21,6 +21,7 @@ import { getSpeechProvider, parseSpeechModel } from "../config/audioRegistry.ts"
|
||||
import { buildAuthHeaders } from "../config/registryUtils.ts";
|
||||
import { kieExecutor } from "../executors/kie.ts";
|
||||
import { vertexGenerateSpeech } from "../executors/vertexMedia.ts";
|
||||
import { handleGeminiTtsSpeech } from "../executors/geminiTts.ts";
|
||||
import { handleAwsPollySpeech } from "../executors/awsPollyTts.ts";
|
||||
import { handleEdgeTtsSpeech } from "../executors/edgeTts.ts";
|
||||
import { GttsUpstreamError, normalizeGttsLang, synthesizeGtts } from "../executors/gtts.ts";
|
||||
@@ -889,6 +890,13 @@ export async function handleAudioSpeech({
|
||||
headers: { ...CORS_HEADERS, "Content-Type": contentType },
|
||||
});
|
||||
}
|
||||
if (providerConfig.format === "gemini-tts") {
|
||||
return handleGeminiTtsSpeech(credentials, {
|
||||
model: modelId,
|
||||
text: body.input,
|
||||
voice: body.voice,
|
||||
});
|
||||
}
|
||||
|
||||
if (providerConfig.format === "hyperbolic") {
|
||||
return handleHyperbolicSpeech(providerConfig, body, token);
|
||||
|
||||
@@ -20,6 +20,7 @@ import { checkSemanticCache } from "./chatCore/semanticCache.ts";
|
||||
import { checkLifecycle, resolveLifecycle } from "./chatCore/modelLifecyclePolicy.ts";
|
||||
import {
|
||||
shouldDefaultAllowClassifier,
|
||||
detectClassifierFormat,
|
||||
buildDefaultAllowClaudeMessage,
|
||||
} from "./chatCore/claudeClassifierCompat.ts";
|
||||
import { applyClientUsageBuffer } from "./chatCore/clientUsageBuffer.ts";
|
||||
@@ -379,6 +380,7 @@ import { isCompactResponsesEndpoint } from "../executors/codex.ts";
|
||||
import { persistCodexChildQuotaResponse } from "../services/codexAccount/index.ts";
|
||||
import { invalidateCodexQuotaCache } from "../services/codexQuotaFetcher.ts";
|
||||
import { translateNonStreamingResponse } from "./responseTranslator.ts";
|
||||
import { extractToolSchemaMap } from "../translator/response/openai-responses/toolSchemas.ts";
|
||||
import { unwrapClineNonStreamingEnvelope } from "./chatCore/clineResponseEnvelope.ts";
|
||||
import { extractUsageFromResponse } from "./usageExtractor.ts";
|
||||
import {
|
||||
@@ -778,11 +780,12 @@ export async function handleChatCore({
|
||||
classifierSettings.claudeClassifierCompat as string | undefined
|
||||
)
|
||||
) {
|
||||
const classifierFormat = detectClassifierFormat(body as Record<string, unknown>);
|
||||
log?.warn?.(
|
||||
"CHAT",
|
||||
`classifier compat=${classifierSettings.claudeClassifierCompat} | short-circuit default-allow`
|
||||
`classifier compat=${classifierSettings.claudeClassifierCompat} format=${classifierFormat} | short-circuit default-allow`
|
||||
);
|
||||
return buildDefaultAllowClaudeMessage(requestedModel);
|
||||
return buildDefaultAllowClaudeMessage(requestedModel, classifierFormat);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1218,7 +1221,12 @@ export async function handleChatCore({
|
||||
credentials?.providerSpecificData?.preserveEncryptedReasoning === true,
|
||||
onIncompatibleReasoning: resolveIncompatibleReasoningAction({
|
||||
reasoningTransportFallback,
|
||||
isComboStep: Boolean(comboStepId || comboExecutionKey),
|
||||
// #11178 regressed combo steps whose combo record carries no explicit
|
||||
// stepId/executionKey (plain model-list combos): their explicit
|
||||
// `reasoningTransportFallback: "skip"` config was silently degraded to
|
||||
// "drop". `isCombo` is the combo marker; step ids are optional
|
||||
// finer-grained metadata that plain combos never set.
|
||||
isComboStep: Boolean(isCombo) || Boolean(comboStepId || comboExecutionKey),
|
||||
headers: clientRawRequest?.headers ?? null,
|
||||
}),
|
||||
}
|
||||
@@ -4905,12 +4913,14 @@ export async function handleChatCore({
|
||||
|
||||
// Translate response to client's expected format (usually OpenAI)
|
||||
// Pass toolNameMap so Claude OAuth proxy_ prefix is stripped in tool_use blocks (#605)
|
||||
const responseToolSchemas = extractToolSchemaMap(finalBody || translatedBody || body);
|
||||
let translatedResponse = needsTranslation(responsePayloadFormat, clientResponseFormat)
|
||||
? translateNonStreamingResponse(
|
||||
responseBody,
|
||||
responsePayloadFormat,
|
||||
clientResponseFormat,
|
||||
responseToolNameMap
|
||||
responseToolNameMap,
|
||||
responseToolSchemas
|
||||
)
|
||||
: responseBody;
|
||||
const memoryExtractionResponse = translatedResponse;
|
||||
@@ -4937,7 +4947,8 @@ export async function handleChatCore({
|
||||
responseBody,
|
||||
responsePayloadFormat,
|
||||
FORMATS.OPENAI,
|
||||
responseToolNameMap
|
||||
responseToolNameMap,
|
||||
responseToolSchemas
|
||||
)
|
||||
: responseBody;
|
||||
const firstChoice = cacheResponse?.choices?.[0];
|
||||
@@ -5460,7 +5471,8 @@ export async function handleChatCore({
|
||||
streamBody,
|
||||
clientResponseFormat,
|
||||
FORMATS.OPENAI,
|
||||
responseToolNameMap
|
||||
responseToolNameMap,
|
||||
extractToolSchemaMap(finalBody || translatedBody || body)
|
||||
) as Record<string, unknown>)
|
||||
: streamBody;
|
||||
const choices = cacheStreamBody.choices as
|
||||
|
||||
@@ -24,14 +24,19 @@ const SECURITY_MONITOR_MARKER = "You are a security monitor for autonomous AI co
|
||||
|
||||
export type ClaudeClassifierCompatMode = "off" | "auto" | "always";
|
||||
|
||||
/** The two synthetic-response shapes Claude Code's classifier can expect. */
|
||||
export type ClaudeClassifierFormat = "block" | "severity";
|
||||
|
||||
function extractSystemTexts(body: Record<string, unknown> | null | undefined): string[] {
|
||||
const system = body?.system;
|
||||
if (typeof system === "string") return [system];
|
||||
if (Array.isArray(system)) {
|
||||
return system
|
||||
.map((part) => (part && typeof (part as { text?: unknown }).text === "string"
|
||||
? ((part as { text: string }).text)
|
||||
: ""))
|
||||
.map((part) =>
|
||||
part && typeof (part as { text?: unknown }).text === "string"
|
||||
? (part as { text: string }).text
|
||||
: ""
|
||||
)
|
||||
.filter(Boolean);
|
||||
}
|
||||
return [];
|
||||
@@ -60,6 +65,29 @@ export function shouldDefaultAllowClassifier(
|
||||
return extractSystemTexts(body).some((text) => text.includes(SECURITY_MONITOR_MARKER));
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect which synthetic-response shape the classifier request expects.
|
||||
*
|
||||
* Newer Claude Code builds send a "severity classifier" variant of the same internal
|
||||
* request: it carries `stop_sequences: [..., "</severity>", ...]` and parses a
|
||||
* `<severity>N</severity>` reply instead of `<block>no</block>`/`<block>yes</block>`.
|
||||
* Feeding it the legacy `<block>no</block>` shape is unparseable, so it retries both
|
||||
* stages and then fails closed — the same "blocking it for safety" failure this compat
|
||||
* shim exists to avoid. Only `stop_sequences` distinguishes the two shapes; callers
|
||||
* should only consult this after `shouldDefaultAllowClassifier` has already confirmed
|
||||
* the request is the classifier (via the system-prompt marker), so an unrelated app
|
||||
* that merely happens to use `</severity>` as a stop token is never affected (#8189).
|
||||
*/
|
||||
export function detectClassifierFormat(
|
||||
body: Record<string, unknown> | null | undefined
|
||||
): ClaudeClassifierFormat {
|
||||
const stopSequences = body?.stop_sequences;
|
||||
if (Array.isArray(stopSequences) && stopSequences.includes("</severity>")) {
|
||||
return "severity";
|
||||
}
|
||||
return "block";
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the synthetic Claude `message` ALLOW response. Always returns a plain JSON
|
||||
* body (matching the upstream reference implementation) — Claude Code's classifier
|
||||
@@ -67,7 +95,10 @@ export function shouldDefaultAllowClassifier(
|
||||
* satisfies both streaming and non-streaming callers without needing to plumb a
|
||||
* synthetic SSE encoding through the streaming/sseToJson/non-streaming handlers.
|
||||
*/
|
||||
export function buildDefaultAllowClaudeMessage(model?: string | null): {
|
||||
export function buildDefaultAllowClaudeMessage(
|
||||
model?: string | null,
|
||||
format: ClaudeClassifierFormat = "block"
|
||||
): {
|
||||
success: true;
|
||||
response: Response;
|
||||
} {
|
||||
@@ -76,7 +107,12 @@ export function buildDefaultAllowClaudeMessage(model?: string | null): {
|
||||
type: "message",
|
||||
role: "assistant",
|
||||
model: model || "claude-3-5-sonnet-20241022",
|
||||
content: [{ type: "text", text: "<block>no</block>" }],
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: format === "severity" ? "<severity>0</severity>" : "<block>no</block>",
|
||||
},
|
||||
],
|
||||
stop_reason: "end_turn",
|
||||
stop_sequence: null,
|
||||
usage: { input_tokens: 1, output_tokens: 1 },
|
||||
|
||||
@@ -91,8 +91,20 @@ interface KieImageOptions {
|
||||
} | null;
|
||||
}
|
||||
|
||||
// KIE Market catalog ids are namespaced for OmniRoute's catalog
|
||||
// (`google-imagen/<model>`), but the KIE Market createTask API expects
|
||||
// vendor-specific upstream ids that do not follow a single consistent
|
||||
// pattern (confirmed against docs.kie.ai/market/google/* — see #11225,
|
||||
// #11296): nano-banana-2 and nano-banana-pro drop the vendor namespace
|
||||
// entirely, while nano-banana and nano-banana-edit use a `google/` prefix
|
||||
// instead of `google-imagen/`. Every other KIE Market namespace (seedream,
|
||||
// flux, ideogram, qwen, wan, grok-imagine, gpt) already matches its real
|
||||
// upstream id byte-for-byte, so this map stays scoped to google-imagen.
|
||||
export const KIE_MARKET_UPSTREAM_MODEL_IDS: ReadonlyMap<string, string> = new Map([
|
||||
["google-imagen/nano-banana", "google/nano-banana"],
|
||||
["google-imagen/nano-banana-2", "nano-banana-2"],
|
||||
["google-imagen/nano-banana-pro", "nano-banana-pro"],
|
||||
["google-imagen/nano-banana-edit", "google/nano-banana-edit"],
|
||||
]);
|
||||
|
||||
export function resolveKieMarketUpstreamModelId(publicModelId: string): string {
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
import { restoreClaudeToolName } from "../services/claudeCodeToolRemapper.ts";
|
||||
import { extractReplayableResponsesReasoningText } from "../services/reasoningInputPolicy.ts";
|
||||
import { sanitizeToolId } from "../translator/helpers/schemaCoercion.ts";
|
||||
import { stripEmptyOptionalToolArgs } from "../translator/response/openai-responses/pureHelpers.ts";
|
||||
|
||||
type JsonRecord = Record<string, unknown>;
|
||||
|
||||
@@ -135,24 +136,28 @@ function findBestMessageText(output: unknown[]): {
|
||||
* Handles different provider response formats (Gemini, Claude, etc.)
|
||||
*
|
||||
* @param toolNameMap - Optional Map<prefixedName, originalName> for Claude OAuth tool name stripping
|
||||
* @param toolSchemas - Optional Map<toolName, parametersSchema> for schema-aware optional-arg cleanup
|
||||
*/
|
||||
export function translateNonStreamingResponse(
|
||||
responseBody: JsonRecord,
|
||||
targetFormat: string,
|
||||
sourceFormat: string,
|
||||
toolNameMap?: Map<string, string> | null
|
||||
toolNameMap?: Map<string, string> | null,
|
||||
toolSchemas?: Map<string, JsonRecord> | null
|
||||
): JsonRecord;
|
||||
export function translateNonStreamingResponse(
|
||||
responseBody: unknown,
|
||||
targetFormat: string,
|
||||
sourceFormat: string,
|
||||
toolNameMap?: Map<string, string> | null
|
||||
toolNameMap?: Map<string, string> | null,
|
||||
toolSchemas?: Map<string, JsonRecord> | null
|
||||
): unknown;
|
||||
export function translateNonStreamingResponse(
|
||||
responseBody: unknown,
|
||||
targetFormat: string,
|
||||
sourceFormat: string,
|
||||
toolNameMap?: Map<string, string> | null
|
||||
toolNameMap?: Map<string, string> | null,
|
||||
toolSchemas?: Map<string, JsonRecord> | null
|
||||
): unknown {
|
||||
// If already in source format, return as-is
|
||||
if (targetFormat === sourceFormat) {
|
||||
@@ -219,6 +224,11 @@ export function translateNonStreamingResponse(
|
||||
toString(itemObj.id) ||
|
||||
`call_${Date.now()}_${toolCalls.length}`;
|
||||
let argsToEmit = itemObj.arguments;
|
||||
const rawName = toString(itemObj.name);
|
||||
const toolSchema = toolSchemas?.get(rawName);
|
||||
if (toolSchema) {
|
||||
argsToEmit = stripEmptyOptionalToolArgs(argsToEmit, rawName, toolSchema);
|
||||
}
|
||||
if (argsToEmit != null && typeof argsToEmit === "object" && !Array.isArray(argsToEmit)) {
|
||||
const cleaned: JsonRecord = { ...(argsToEmit as JsonRecord) };
|
||||
for (const [k, v] of Object.entries(cleaned)) {
|
||||
@@ -229,7 +239,6 @@ export function translateNonStreamingResponse(
|
||||
|
||||
const fnArgs =
|
||||
typeof argsToEmit === "string" ? argsToEmit : JSON.stringify(argsToEmit || {});
|
||||
const rawName = toString(itemObj.name);
|
||||
// Strip Claude OAuth proxy_ prefix using toolNameMap
|
||||
const resolvedName = caseInsensitiveToolNameLookup(rawName, toolNameMap) ?? rawName;
|
||||
toolCalls.push({
|
||||
|
||||
@@ -31,6 +31,7 @@ import * as xSearch from "./search/xSearch.ts";
|
||||
import { freeWebSearch } from "../services/freeWebSearch.ts";
|
||||
import { saveCallLog } from "@/lib/usageDb";
|
||||
import { safeOutboundFetch } from "@/shared/network/safeOutboundFetch";
|
||||
import { parseAndValidateNonMetadataUrl } from "@/shared/network/outboundUrlGuard";
|
||||
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
||||
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
||||
import { z } from "zod";
|
||||
@@ -313,9 +314,23 @@ function getProviderSettingString(
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function resolveSearchBaseUrl(config: SearchProviderConfig, params: SearchRequestParams): string {
|
||||
export function resolveSearchBaseUrl(
|
||||
config: SearchProviderConfig,
|
||||
params: SearchRequestParams
|
||||
): string {
|
||||
const override = getProviderSettingString(params, "baseUrl");
|
||||
return (override || config.baseUrl).replace(/\/+$/, "");
|
||||
if (override) {
|
||||
// GHSA-j7j4-g9qc-q69c: the override is client-controlled (provider_options /
|
||||
// providerSpecificData) and flows into a plain fetch() sink — validate it
|
||||
// before any builder uses it as the server-side fetch target. Mode is
|
||||
// block-metadata (NOT public-only): the primary searxng use case is a
|
||||
// self-hosted instance on loopback/LAN, so private hosts keep working,
|
||||
// while cloud-metadata endpoints (IMDS credential theft) are rejected.
|
||||
// The catalog's own config.baseUrl is operator config and stays untouched.
|
||||
parseAndValidateNonMetadataUrl(override);
|
||||
return override.replace(/\/+$/, "");
|
||||
}
|
||||
return config.baseUrl.replace(/\/+$/, "");
|
||||
}
|
||||
|
||||
function toSearchPageNumber(offset: number | undefined, maxResults: number): number | undefined {
|
||||
|
||||
40
open-sse/services/compression/compressionWorker.ts
Normal file
40
open-sse/services/compression/compressionWorker.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { parentPort } from "node:worker_threads";
|
||||
import {
|
||||
applyCompression,
|
||||
applyStackedCompression,
|
||||
type StackedCompressionStep,
|
||||
} from "./strategySelector.ts";
|
||||
import type {
|
||||
CompressionWorkerJob,
|
||||
CompressionWorkerMessage,
|
||||
} from "./compressionWorkerProtocol.ts";
|
||||
|
||||
if (!parentPort) throw new Error("compressionWorker must run in a worker thread");
|
||||
parentPort.on("message", (job: CompressionWorkerJob) => {
|
||||
try {
|
||||
const onEngineStep = (step: StackedCompressionStep) =>
|
||||
parentPort.postMessage({
|
||||
id: job.id,
|
||||
type: "step",
|
||||
step,
|
||||
} satisfies CompressionWorkerMessage);
|
||||
const result =
|
||||
job.mode === "stacked"
|
||||
? applyStackedCompression(job.body, job.options?.config?.stackedPipeline, {
|
||||
...job.options,
|
||||
onEngineStep,
|
||||
})
|
||||
: applyCompression(job.body, job.mode, job.options);
|
||||
parentPort.postMessage({
|
||||
id: job.id,
|
||||
type: "result",
|
||||
result,
|
||||
} satisfies CompressionWorkerMessage);
|
||||
} catch (error) {
|
||||
parentPort.postMessage({
|
||||
id: job.id,
|
||||
type: "error",
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
} satisfies CompressionWorkerMessage);
|
||||
}
|
||||
});
|
||||
164
open-sse/services/compression/compressionWorkerPool.ts
Normal file
164
open-sse/services/compression/compressionWorkerPool.ts
Normal file
@@ -0,0 +1,164 @@
|
||||
import { existsSync } from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { Worker } from "node:worker_threads";
|
||||
import type { CompressionResult } from "./types.ts";
|
||||
import type { StackedCompressionStep } from "./strategySelector.ts";
|
||||
import type {
|
||||
CompressionWorkerJob,
|
||||
CompressionWorkerMessage,
|
||||
CompressionWorkerOptions,
|
||||
} from "./compressionWorkerProtocol.ts";
|
||||
|
||||
function positiveInteger(value: string | undefined, fallback: number): number {
|
||||
const parsed = Number(value);
|
||||
return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : fallback;
|
||||
}
|
||||
function workerUrl(): URL {
|
||||
const dir = dirname(fileURLToPath(import.meta.url));
|
||||
for (const name of ["compressionWorker.js", "compressionWorker.ts"]) {
|
||||
if (existsSync(join(dir, name))) return new URL(name, import.meta.url);
|
||||
}
|
||||
return new URL("compressionWorker.js", import.meta.url);
|
||||
}
|
||||
function unchanged(body: Record<string, unknown>): CompressionResult {
|
||||
return { body, compressed: false, stats: null };
|
||||
}
|
||||
interface PendingJob extends CompressionWorkerJob {
|
||||
originalBody: Record<string, unknown>;
|
||||
resolve: (result: CompressionResult) => void;
|
||||
onEngineStep?: (step: StackedCompressionStep) => void;
|
||||
}
|
||||
interface PoolWorker {
|
||||
worker: Worker;
|
||||
job: PendingJob | null;
|
||||
timeout: NodeJS.Timeout | null;
|
||||
idle: NodeJS.Timeout | null;
|
||||
}
|
||||
|
||||
export class CompressionWorkerPool {
|
||||
private readonly queue: PendingJob[] = [];
|
||||
private readonly workers = new Set<PoolWorker>();
|
||||
private nextId = 1;
|
||||
private readonly size: number;
|
||||
private readonly timeoutMs: number;
|
||||
private readonly idleMs: number;
|
||||
|
||||
constructor({
|
||||
size = positiveInteger(process.env.OMNI_COMPRESSION_WORKERS, 2),
|
||||
timeoutMs = positiveInteger(process.env.OMNI_COMPRESSION_WORKER_TIMEOUT_MS, 120_000),
|
||||
idleMs = positiveInteger(process.env.OMNI_COMPRESSION_WORKER_IDLE_MS, 60_000),
|
||||
}: { size?: number; timeoutMs?: number; idleMs?: number } = {}) {
|
||||
this.size = Math.max(1, Math.floor(size));
|
||||
this.timeoutMs = Math.max(1, Math.floor(timeoutMs));
|
||||
this.idleMs = Math.max(1, Math.floor(idleMs));
|
||||
}
|
||||
|
||||
run(
|
||||
body: Record<string, unknown>,
|
||||
mode: CompressionWorkerJob["mode"],
|
||||
options?: CompressionWorkerOptions,
|
||||
onEngineStep?: (step: StackedCompressionStep) => void
|
||||
): Promise<CompressionResult> {
|
||||
return new Promise((resolve) => {
|
||||
this.queue.push({
|
||||
id: this.nextId++,
|
||||
body,
|
||||
mode,
|
||||
options,
|
||||
originalBody: body,
|
||||
resolve,
|
||||
onEngineStep,
|
||||
});
|
||||
this.dispatch();
|
||||
});
|
||||
}
|
||||
async close(): Promise<void> {
|
||||
for (const job of this.queue.splice(0)) job.resolve(unchanged(job.originalBody));
|
||||
await Promise.all([...this.workers].map((slot) => this.remove(slot, true)));
|
||||
}
|
||||
private spawn(): PoolWorker {
|
||||
const slot: PoolWorker = {
|
||||
worker: new Worker(workerUrl()),
|
||||
job: null,
|
||||
timeout: null,
|
||||
idle: null,
|
||||
};
|
||||
this.workers.add(slot);
|
||||
slot.worker.on("message", (message: CompressionWorkerMessage) =>
|
||||
this.handleMessage(slot, message)
|
||||
);
|
||||
slot.worker.on("error", () => this.fail(slot));
|
||||
slot.worker.on("exit", () => {
|
||||
if (this.workers.has(slot)) this.fail(slot);
|
||||
});
|
||||
return slot;
|
||||
}
|
||||
private dispatch(): void {
|
||||
while (this.queue.length) {
|
||||
let slot = [...this.workers].find((candidate) => !candidate.job);
|
||||
if (!slot && this.workers.size < this.size) slot = this.spawn();
|
||||
if (!slot) return;
|
||||
if (slot.idle) clearTimeout(slot.idle);
|
||||
const job = this.queue.shift();
|
||||
if (!job) return;
|
||||
slot.job = job;
|
||||
slot.timeout = setTimeout(() => this.fail(slot!), this.timeoutMs);
|
||||
slot.timeout.unref();
|
||||
const { originalBody: _body, resolve: _resolve, onEngineStep: _step, ...wireJob } = job;
|
||||
slot.worker.postMessage(wireJob);
|
||||
}
|
||||
}
|
||||
private handleMessage(slot: PoolWorker, message: CompressionWorkerMessage): void {
|
||||
const job = slot.job;
|
||||
if (!job || job.id !== message.id) return;
|
||||
if (message.type === "step") {
|
||||
try {
|
||||
job.onEngineStep?.(message.step);
|
||||
} catch {
|
||||
// Telemetry is best-effort.
|
||||
}
|
||||
return;
|
||||
}
|
||||
this.finish(slot, message.type === "result" ? message.result : unchanged(job.originalBody));
|
||||
}
|
||||
private finish(slot: PoolWorker, result: CompressionResult): void {
|
||||
const job = slot.job;
|
||||
if (!job) return;
|
||||
if (slot.timeout) clearTimeout(slot.timeout);
|
||||
slot.timeout = null;
|
||||
slot.job = null;
|
||||
job.resolve(result);
|
||||
slot.idle = setTimeout(() => void this.remove(slot, false), this.idleMs);
|
||||
slot.idle.unref();
|
||||
this.dispatch();
|
||||
}
|
||||
private fail(slot: PoolWorker): void {
|
||||
const job = slot.job;
|
||||
if (job) job.resolve(unchanged(job.originalBody));
|
||||
slot.job = null;
|
||||
void this.remove(slot, true).finally(() => this.dispatch());
|
||||
}
|
||||
private async remove(slot: PoolWorker, terminate: boolean): Promise<void> {
|
||||
if (!this.workers.delete(slot)) return;
|
||||
if (slot.timeout) clearTimeout(slot.timeout);
|
||||
if (slot.idle) clearTimeout(slot.idle);
|
||||
if (terminate) await slot.worker.terminate().catch(() => undefined);
|
||||
}
|
||||
}
|
||||
|
||||
let pool: CompressionWorkerPool | null = null;
|
||||
export function runCompressionInWorker(
|
||||
body: Record<string, unknown>,
|
||||
mode: CompressionWorkerJob["mode"],
|
||||
options?: CompressionWorkerOptions,
|
||||
onEngineStep?: (step: StackedCompressionStep) => void
|
||||
): Promise<CompressionResult> {
|
||||
pool ??= new CompressionWorkerPool();
|
||||
return pool.run(body, mode, options, onEngineStep);
|
||||
}
|
||||
export async function closeCompressionWorkerPoolForTests(): Promise<void> {
|
||||
const active = pool;
|
||||
pool = null;
|
||||
await active?.close();
|
||||
}
|
||||
71
open-sse/services/compression/compressionWorkerProtocol.ts
Normal file
71
open-sse/services/compression/compressionWorkerProtocol.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import type { CompressionConfig, CompressionMode, CompressionResult } from "./types.ts";
|
||||
import type { StackedCompressionStep } from "./strategySelector.ts";
|
||||
import type {
|
||||
CompressionStage,
|
||||
CompressionWireFormat,
|
||||
ImageTransportFidelity,
|
||||
} from "./engines/types.ts";
|
||||
|
||||
export interface CompressionWorkerOptions {
|
||||
model?: string;
|
||||
supportsVision?: boolean | null;
|
||||
providerTransport?: "direct" | "aggregator";
|
||||
provider?: string;
|
||||
imageTransportFidelity?: ImageTransportFidelity;
|
||||
sourceFormat?: CompressionWireFormat;
|
||||
targetFormat?: CompressionWireFormat;
|
||||
compressionStage?: CompressionStage;
|
||||
config?: CompressionConfig;
|
||||
}
|
||||
export interface CompressionWorkerJob {
|
||||
id: number;
|
||||
body: Record<string, unknown>;
|
||||
mode: CompressionMode;
|
||||
options?: CompressionWorkerOptions;
|
||||
}
|
||||
export type CompressionWorkerMessage =
|
||||
| { id: number; type: "step"; step: StackedCompressionStep }
|
||||
| { id: number; type: "result"; result: CompressionResult }
|
||||
| { id: number; type: "error"; error: string };
|
||||
|
||||
function isPlainObject(value: object): value is Record<string, unknown> {
|
||||
const prototype = Object.getPrototypeOf(value);
|
||||
return prototype === Object.prototype || prototype === null;
|
||||
}
|
||||
export function isStrictlySerializable(value: unknown, seen = new Set<object>()): boolean {
|
||||
if (
|
||||
value === null ||
|
||||
typeof value === "string" ||
|
||||
typeof value === "boolean" ||
|
||||
typeof value === "number"
|
||||
) {
|
||||
return typeof value !== "number" || Number.isFinite(value);
|
||||
}
|
||||
if (typeof value !== "object" || seen.has(value)) return false;
|
||||
seen.add(value);
|
||||
if (Array.isArray(value)) return value.every((entry) => isStrictlySerializable(entry, seen));
|
||||
if (!isPlainObject(value)) return false;
|
||||
return Object.values(value).every((entry) => isStrictlySerializable(entry, seen));
|
||||
}
|
||||
|
||||
const WORKER_STACK_ENGINES = new Set(["caveman", "rtk", "standard"]);
|
||||
export function isCompressionWorkerEligible(
|
||||
body: Record<string, unknown>,
|
||||
mode: CompressionMode,
|
||||
options?: CompressionWorkerOptions
|
||||
): boolean {
|
||||
if (mode !== "standard" && mode !== "rtk" && mode !== "stacked") return false;
|
||||
if (mode === "stacked") {
|
||||
const pipeline = options?.config?.stackedPipeline;
|
||||
if (!Array.isArray(pipeline) || pipeline.length === 0) return false;
|
||||
if (
|
||||
pipeline.some((step) => {
|
||||
const engine = typeof step === "string" ? step : step.engine;
|
||||
return !WORKER_STACK_ENGINES.has(engine);
|
||||
})
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return isStrictlySerializable({ body, mode, ...(options ? { options } : {}) });
|
||||
}
|
||||
@@ -519,6 +519,28 @@ async function runCompressionAsync(
|
||||
cachingContext?: CachingDetectionContext;
|
||||
}
|
||||
): Promise<CompressionResult> {
|
||||
const workerOptions = options
|
||||
? {
|
||||
model: options.model,
|
||||
supportsVision: options.supportsVision,
|
||||
providerTransport: options.providerTransport,
|
||||
provider: options.provider,
|
||||
imageTransportFidelity: options.imageTransportFidelity,
|
||||
sourceFormat: options.sourceFormat,
|
||||
targetFormat: options.targetFormat,
|
||||
compressionStage: options.compressionStage,
|
||||
config: options.config,
|
||||
}
|
||||
: undefined;
|
||||
const { isCompressionWorkerEligible } = await import("./compressionWorkerProtocol.ts");
|
||||
if (isCompressionWorkerEligible(body, mode, workerOptions)) {
|
||||
try {
|
||||
const { runCompressionInWorker } = await import("./compressionWorkerPool.ts");
|
||||
return await runCompressionInWorker(body, mode, workerOptions, options?.onEngineStep);
|
||||
} catch {
|
||||
return { body, compressed: false, stats: null };
|
||||
}
|
||||
}
|
||||
if (
|
||||
options?.config?.memoizeCompressionResults === true &&
|
||||
// Only memoize for an explicit principal — a missing principalId would collapse
|
||||
|
||||
@@ -12,7 +12,21 @@
|
||||
* `sanitizeReasoningEffortForProvider` in `executors/base/reasoningEffort.ts`)
|
||||
* so the 4xx→retry round-trip is paid at most once per process per provider+model.
|
||||
*
|
||||
* `clampToLearned` implements downgrade-only clamping: greatest accepted <= demand.
|
||||
* `clampToLearned` implements nearest-tier clamping: smallest accepted >= demand,
|
||||
* falling back to the greatest accepted when demand exceeds every accepted value.
|
||||
* (#11295 — unified with the static "declared" clamp in
|
||||
* `executors/base/reasoningEffort.ts`, which already used nearest-tier semantics.
|
||||
* Before #11295, this learned clamp was downgrade-only — greatest accepted <=
|
||||
* demand — so the SAME accepted set {low,high,max} produced medium→low here but
|
||||
* medium→high via the declared path: identical inputs, opposite outputs,
|
||||
* depending only on whether the model had a static registry entry. #11274's
|
||||
* DeepSeek native mapping is the precedent for nearest-tier. This also fixes a
|
||||
* standalone bug: a request BELOW the learned floor (e.g. none/minimal on a
|
||||
* model that only ever advertised {low,high,max}) used to return null — no
|
||||
* clamp — so the too-low value passed straight through to the upstream, which
|
||||
* 400'd again on every subsequent request without ever learning a lower floor.
|
||||
* Nearest-tier naturally fixes this too: the smallest accepted value is always
|
||||
* >= any demand below the floor, so it is returned instead of null.
|
||||
*
|
||||
* In-memory only (same operator-accepted tradeoff as the thinking-budget cache):
|
||||
* restart resets, the first request after a restart may re-learn at the cost of
|
||||
@@ -132,25 +146,39 @@ export function recordLearnedReasoningEffort(
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the greatest accepted value <= effortStr (downgrade only), or null
|
||||
* if effortStr is already accepted, below the minimum, or not in ORDER.
|
||||
* Return the nearest-tier accepted value for effortStr: the smallest accepted
|
||||
* value with rank >= effortStr's rank, or — when effortStr's rank exceeds every
|
||||
* accepted value (demand above the learned ceiling) — the greatest accepted
|
||||
* value. Returns null only when effortStr is already accepted (no clamp
|
||||
* needed), empty, or not a recognized member of REASONING_EFFORT_ORDER.
|
||||
*
|
||||
* Mirrors the declared-capability clamp in `executors/base/reasoningEffort.ts`
|
||||
* (#11295): both now use nearest-tier semantics so the same accepted set
|
||||
* produces the same mapping regardless of whether the model has a static
|
||||
* registry entry or was only learned reactively from an upstream 4xx.
|
||||
*/
|
||||
export function clampToLearned(effortStr: string, accepted: Set<string>): string | null {
|
||||
if (!effortStr || accepted.has(effortStr)) return null;
|
||||
const rank = rankOf(effortStr);
|
||||
if (rank === -1) return null;
|
||||
const minRank = Math.min(...[...accepted].map((v) => rankOf(v)));
|
||||
if (rank < minRank) return null;
|
||||
let best: string | null = null;
|
||||
let bestRank = -1;
|
||||
|
||||
let nearestAbove: string | null = null;
|
||||
let nearestAboveRank = Infinity;
|
||||
let highest: string | null = null;
|
||||
let highestRank = -1;
|
||||
for (const v of accepted) {
|
||||
const r = rankOf(v);
|
||||
if (r <= rank && r > bestRank) {
|
||||
bestRank = r;
|
||||
best = v;
|
||||
if (r < 0) continue;
|
||||
if (r >= rank && r < nearestAboveRank) {
|
||||
nearestAboveRank = r;
|
||||
nearestAbove = v;
|
||||
}
|
||||
if (r > highestRank) {
|
||||
highestRank = r;
|
||||
highest = v;
|
||||
}
|
||||
}
|
||||
return best;
|
||||
return nearestAbove ?? highest;
|
||||
}
|
||||
|
||||
// Matches prose shapes: OVH's "@ai-sdk/openai-compatible" deserializer
|
||||
|
||||
@@ -28,12 +28,10 @@ export async function refreshCopilotToken(
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
log?.error?.("TOKEN_REFRESH", "Failed to refresh Copilot token", {
|
||||
status: response.status,
|
||||
error: errorText,
|
||||
});
|
||||
return null;
|
||||
return { status: response.status };
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
@@ -49,8 +47,8 @@ export async function refreshCopilotToken(
|
||||
};
|
||||
} catch (error) {
|
||||
log?.error?.("TOKEN_REFRESH", "Error refreshing Copilot token", {
|
||||
error: error.message,
|
||||
errorType: error?.name || "Error",
|
||||
});
|
||||
return null;
|
||||
return { status: null };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -290,6 +290,31 @@ export function coerceToolSchemas(tools: unknown): unknown {
|
||||
});
|
||||
}
|
||||
|
||||
const NULL_OMISSION_NOTE = "null = omit this parameter";
|
||||
|
||||
function schemaTypeIncludes(type: unknown, wanted: string): boolean {
|
||||
return type === wanted || (Array.isArray(type) && type.includes(wanted));
|
||||
}
|
||||
|
||||
function isPlainStringType(type: unknown): boolean {
|
||||
return type === "string" || (Array.isArray(type) && type.length === 1 && type[0] === "string");
|
||||
}
|
||||
|
||||
function appendNullOmissionMarker(description: unknown): string {
|
||||
if (typeof description === "string" && description.length > 0) {
|
||||
return description.includes(NULL_OMISSION_NOTE)
|
||||
? description
|
||||
: `${description} (${NULL_OMISSION_NOTE})`;
|
||||
}
|
||||
return NULL_OMISSION_NOTE;
|
||||
}
|
||||
|
||||
function widenTypeWithNull(type: unknown): unknown {
|
||||
if (typeof type === "string") return [type, "null"];
|
||||
if (Array.isArray(type) && !type.includes("null")) return [...type, "null"];
|
||||
return type;
|
||||
}
|
||||
|
||||
// #7023 — Responses API strict mode forces every "optional" tool property into
|
||||
// `required`, so a model that intends to OMIT an optional enum property (no declared
|
||||
// `default`) must still emit a concrete value (e.g. Agent.isolation:"remote"). Neither
|
||||
@@ -299,7 +324,11 @@ export function coerceToolSchemas(tools: unknown): unknown {
|
||||
// `null` (see pureHelpers.ts::isDroppableNullEntry). Scope: top-level
|
||||
// `properties[key].enum` only — does not recurse into `items`/`anyOf`/`oneOf` branches
|
||||
// (no real-world case beyond Agent.isolation is documented; extend with a concrete repro).
|
||||
function shouldInjectNullOmission(key: string, propSchema: unknown, required: Set<string>): boolean {
|
||||
function shouldInjectNullOmission(
|
||||
key: string,
|
||||
propSchema: unknown,
|
||||
required: Set<string>
|
||||
): boolean {
|
||||
return (
|
||||
isPlainObject(propSchema) &&
|
||||
Array.isArray(propSchema.enum) &&
|
||||
@@ -312,19 +341,38 @@ function widenPropertyForNullOmission(propSchema: JsonRecord): JsonRecord {
|
||||
const widened: JsonRecord = { ...propSchema };
|
||||
const enumValues = propSchema.enum as unknown[];
|
||||
widened.enum = enumValues.includes(null) ? enumValues : [...enumValues, null];
|
||||
if (typeof propSchema.type === "string") {
|
||||
widened.type = [propSchema.type, "null"];
|
||||
} else if (Array.isArray(propSchema.type) && !propSchema.type.includes("null")) {
|
||||
widened.type = [...propSchema.type, "null"];
|
||||
}
|
||||
const note = "null = omit this parameter";
|
||||
widened.description =
|
||||
typeof propSchema.description === "string" && propSchema.description.length > 0
|
||||
? `${propSchema.description} (${note})`
|
||||
: note;
|
||||
widened.type = widenTypeWithNull(propSchema.type);
|
||||
widened.description = appendNullOmissionMarker(propSchema.description);
|
||||
return widened;
|
||||
}
|
||||
|
||||
// OpenCode `subagent.sessionID` (and any other optional default-less plain string) has
|
||||
// the same strict-mode omission problem as #7023 enums, but no enum to widen. Inject
|
||||
// the same nullable-union sentinel on top-level `properties[key]` only — do not recurse
|
||||
// into `items`/`anyOf`/`$defs`, and do not touch enums (owned by the helper above).
|
||||
function shouldInjectStringNullOmission(
|
||||
key: string,
|
||||
propSchema: unknown,
|
||||
required: Set<string>
|
||||
): boolean {
|
||||
return (
|
||||
isPlainObject(propSchema) &&
|
||||
!Array.isArray(propSchema.enum) &&
|
||||
isPlainStringType(propSchema.type) &&
|
||||
!schemaTypeIncludes(propSchema.type, "null") &&
|
||||
!required.has(key) &&
|
||||
!hasOwn(propSchema, "default")
|
||||
);
|
||||
}
|
||||
|
||||
function widenStringPropertyForNullOmission(propSchema: JsonRecord): JsonRecord {
|
||||
return {
|
||||
...propSchema,
|
||||
type: widenTypeWithNull(propSchema.type),
|
||||
description: appendNullOmissionMarker(propSchema.description),
|
||||
};
|
||||
}
|
||||
|
||||
export function injectOptionalEnumOmissionSentinel(schema: unknown): unknown {
|
||||
if (!isPlainObject(schema) || !isPlainObject(schema.properties)) return schema;
|
||||
|
||||
@@ -356,6 +404,43 @@ export function injectOptionalEnumOmissionForTools(tools: unknown): unknown {
|
||||
});
|
||||
}
|
||||
|
||||
export function injectOptionalStringOmissionSentinel(schema: unknown): unknown {
|
||||
if (!isPlainObject(schema) || !isPlainObject(schema.properties)) return schema;
|
||||
|
||||
const required = new Set(Array.isArray(schema.required) ? schema.required : []);
|
||||
let changed = false;
|
||||
const nextProperties: JsonRecord = { ...schema.properties };
|
||||
|
||||
for (const [key, propSchema] of Object.entries(schema.properties)) {
|
||||
if (!shouldInjectStringNullOmission(key, propSchema, required)) continue;
|
||||
nextProperties[key] = widenStringPropertyForNullOmission(propSchema as JsonRecord);
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (!changed) return schema;
|
||||
return { ...schema, properties: nextProperties };
|
||||
}
|
||||
|
||||
export function injectOptionalStringOmissionForTools(tools: unknown): unknown {
|
||||
if (!Array.isArray(tools)) return tools;
|
||||
|
||||
return tools.map((tool) => {
|
||||
if (!isPlainObject(tool)) return tool;
|
||||
|
||||
const result: JsonRecord = { ...tool };
|
||||
if (isPlainObject(result.function) && "parameters" in result.function) {
|
||||
result.function = {
|
||||
...result.function,
|
||||
parameters: injectOptionalStringOmissionSentinel(result.function.parameters),
|
||||
};
|
||||
}
|
||||
if ("parameters" in result && !isPlainObject(result.function)) {
|
||||
result.parameters = injectOptionalStringOmissionSentinel(result.parameters);
|
||||
}
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
export function sanitizeToolDescriptions(tools: unknown): unknown {
|
||||
if (!Array.isArray(tools)) return tools;
|
||||
return tools.map((tool) => sanitizeToolDescription(tool));
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
coerceToolSchemas,
|
||||
injectEmptyReasoningContentForToolCalls,
|
||||
injectOptionalEnumOmissionForTools,
|
||||
injectOptionalStringOmissionForTools,
|
||||
sanitizeToolDescriptions,
|
||||
} from "./helpers/schemaCoercion.ts";
|
||||
import { getRequestTranslator, getResponseTranslator } from "./registry.ts";
|
||||
@@ -595,6 +596,12 @@ export function translateRequest(
|
||||
}
|
||||
|
||||
if (result.tools !== undefined) {
|
||||
// Plain-string omission must run before coerceToolSchemas() strips `default`,
|
||||
// so defaulted optional strings stay unsentinelled. Enum injection stays after
|
||||
// coercion to preserve the #7023 pipeline.
|
||||
if (targetFormat === FORMATS.OPENAI_RESPONSES) {
|
||||
result.tools = injectOptionalStringOmissionForTools(result.tools);
|
||||
}
|
||||
result.tools = coerceToolSchemas(result.tools);
|
||||
result.tools = sanitizeToolDescriptions(result.tools);
|
||||
if (targetFormat === FORMATS.OPENAI_RESPONSES) {
|
||||
|
||||
@@ -866,13 +866,13 @@ export function openaiResponsesToOpenAIResponse(chunk, state) {
|
||||
|
||||
function openaiResponsesToOpenAIResponseStream(chunk, state) {
|
||||
if (!chunk) {
|
||||
// Iterate every still-open call needing schema-aware normalization, not just a
|
||||
// single one — multiple parallel calls can each be pending here if the stream
|
||||
// ends before their output_item.done arrives.
|
||||
// Iterate every still-open call with a buffered argument payload — argument
|
||||
// deltas are buffered for every tool, so an incomplete stream must flush every
|
||||
// buffered call, not only the historical uppercase Agent path.
|
||||
const pendingNormalized: Array<{ index: number; argsStr: string }> = [];
|
||||
if (state.toolCallByCallId instanceof Map) {
|
||||
for (const entry of state.toolCallByCallId.values()) {
|
||||
if (entry.needsNormalization && entry.argsBuffer) {
|
||||
if (entry.argsBuffer) {
|
||||
const toolSchema = state.toolSchemas?.get(entry.name);
|
||||
const argsToEmit = stripEmptyOptionalToolArgs(entry.argsBuffer, entry.name, toolSchema);
|
||||
pendingNormalized.push({
|
||||
|
||||
@@ -56,21 +56,35 @@ function isDroppableEmptyEntry(entry, propSchema, required, key, allowlisted) {
|
||||
return allowlisted || (propSchema != null && !required.has(key));
|
||||
}
|
||||
|
||||
// #7023 — the request-side counterpart (injectOptionalEnumOmissionSentinel) widens
|
||||
// no-default optional enum properties to accept `null`, meaning "omitted" (OpenAI's own
|
||||
// nullable-union idiom for Responses-API strict mode). Drop the key when the model
|
||||
// follows that idiom for a non-required, schema-declared property.
|
||||
function schemaTypeIncludes(type, wanted) {
|
||||
return type === wanted || (Array.isArray(type) && type.includes(wanted));
|
||||
}
|
||||
|
||||
function hasOmissionSentinel(propSchema) {
|
||||
if (!propSchema || typeof propSchema !== "object") return false;
|
||||
if (
|
||||
typeof propSchema.description !== "string" ||
|
||||
!propSchema.description.includes("null = omit this parameter")
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
schemaTypeIncludes(propSchema.type, "null") ||
|
||||
(Array.isArray(propSchema.enum) && propSchema.enum.includes(null))
|
||||
);
|
||||
}
|
||||
|
||||
// #7023 — the request-side counterpart widens no-default optional properties to accept
|
||||
// `null`, meaning "omitted" (OpenAI's own nullable-union idiom for Responses-API strict
|
||||
// mode). Enums use injectOptionalEnumOmissionSentinel; plain strings use
|
||||
// injectOptionalStringOmissionSentinel. Drop the key when the model follows that idiom
|
||||
// for a non-required, schema-declared property, or when OmniRoute's marker is present
|
||||
// even after an upstream strictifies the field into `required`.
|
||||
function isDroppableNullEntry(entry, propSchema, required, key, toolName) {
|
||||
if (entry !== null) return false;
|
||||
if (toolName === "Agent") return true;
|
||||
if (propSchema == null) return false;
|
||||
const omissionSentinel =
|
||||
typeof propSchema === "object" &&
|
||||
Array.isArray(propSchema.enum) &&
|
||||
propSchema.enum.includes(null) &&
|
||||
typeof propSchema.description === "string" &&
|
||||
propSchema.description.includes("null = omit this parameter");
|
||||
return !required.has(key) || omissionSentinel;
|
||||
return !required.has(key) || hasOmissionSentinel(propSchema);
|
||||
}
|
||||
|
||||
function stripEmptyOptionalToolArgsObject(value, toolName, schema) {
|
||||
@@ -110,7 +124,11 @@ export function stripEmptyOptionalToolArgs(value, toolName, schema) {
|
||||
// supplied (schema-aware normalization is not restricted to the allowlist).
|
||||
// "Agent" also passes without a schema: isDroppableNullEntry drops its null
|
||||
// omission sentinels even when the strict schema snapshot is unavailable (#9423).
|
||||
if (!hasUsableSchema(schema) && !STRIPPABLE_EMPTY_ARG_TOOLS.has(toolName) && toolName !== "Agent") {
|
||||
if (
|
||||
!hasUsableSchema(schema) &&
|
||||
!STRIPPABLE_EMPTY_ARG_TOOLS.has(toolName) &&
|
||||
toolName !== "Agent"
|
||||
) {
|
||||
return value;
|
||||
}
|
||||
try {
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
packages:
|
||||
- "packages/*"
|
||||
- "open-sse"
|
||||
# Match `.npmrc`'s legacy-peer-deps posture. OmniRoute imports only the deep
|
||||
# icon modules from @lobehub/icons; auto-installing its unused @lobehub/ui peer
|
||||
# pulls a large UI subtree (including packages without distributable licenses).
|
||||
autoInstallPeers: false
|
||||
allowBuilds:
|
||||
"@parcel/watcher": true
|
||||
"@swc/core": true
|
||||
|
||||
36
scripts/build/better-sqlite3-stub-flag.mjs
Normal file
36
scripts/build/better-sqlite3-stub-flag.mjs
Normal file
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* Decide whether the Next.js build should alias `better-sqlite3` to the
|
||||
* build-time stub (src/lib/db/better-sqlite3.stub.js).
|
||||
*
|
||||
* History (#11343): the alias was UNCONDITIONAL, added to keep the bundler from
|
||||
* tracing the native addon into a Next.js build worker, whose thread teardown
|
||||
* can abort with SIGABRT (assertion in node::RemoveEnvironmentCleanupHook) and
|
||||
* leave the build without standalone output (#10060).
|
||||
*
|
||||
* The premise recorded next to that alias — "runtime still uses the real
|
||||
* package via serverExternalPackages" — does not hold. A Turbopack
|
||||
* `resolveAlias` rewrites the request BEFORE the externals check runs, so
|
||||
* `better-sqlite3` becomes a relative path, no longer matches the
|
||||
* `serverExternalPackages` entry, and the stub is baked into the bundle. Every
|
||||
* artifact built from that config answered HTTP 500 on every route: the stub's
|
||||
* default export is not a constructor, the sync driver chain fell through to
|
||||
* `node:sqlite` and then sql.js, and the instrumentation hook aborted at boot.
|
||||
*
|
||||
* This is the same failure shape as #6344 (the @/mitm/manager stub shipping to
|
||||
* every npm/Electron/VPS artifact), so it gets the same treatment: the alias is
|
||||
* opt-in, and a default build gets the real, externalized native package.
|
||||
*
|
||||
* Set OMNIROUTE_BETTER_SQLITE3_STUB=1 ONLY on a build host that actually hits
|
||||
* the SIGABRT worker teardown, and never for an artifact that will be run —
|
||||
* the resulting bundle cannot open a database.
|
||||
*/
|
||||
export function shouldStubBetterSqlite3(env = process.env) {
|
||||
return env.OMNIROUTE_BETTER_SQLITE3_STUB === "1";
|
||||
}
|
||||
|
||||
/** Turbopack resolveAlias fragment for `better-sqlite3`, derived from the env. */
|
||||
export function betterSqlite3AliasFor(env = process.env) {
|
||||
return shouldStubBetterSqlite3(env)
|
||||
? { "better-sqlite3": "./src/lib/db/better-sqlite3.stub.js" }
|
||||
: {};
|
||||
}
|
||||
@@ -33,6 +33,14 @@ const STANDALONE = process.env.OMNIROUTE_STANDALONE_DIR
|
||||
|
||||
const CALL_LOG_WORKER_REL = join("src", "lib", "usage", "callLogArtifactWorker.js");
|
||||
const CALL_LOG_WORKER_SRC = join(ROOT, "src", "lib", "usage", "callLogArtifactWorker.ts");
|
||||
const COMPRESSION_WORKER_REL = join("open-sse", "services", "compression", "compressionWorker.js");
|
||||
const COMPRESSION_WORKER_SRC = join(
|
||||
ROOT,
|
||||
"open-sse",
|
||||
"services",
|
||||
"compression",
|
||||
"compressionWorker.ts"
|
||||
);
|
||||
const WORKER_REL = join(
|
||||
"open-sse",
|
||||
"services",
|
||||
@@ -107,9 +115,26 @@ function main() {
|
||||
);
|
||||
console.log("[colocate-standalone] ✅ call-log artifact worker bundled");
|
||||
|
||||
const compressionWorkerDest = join(STANDALONE, COMPRESSION_WORKER_REL);
|
||||
mkdirSync(dirname(compressionWorkerDest), { recursive: true });
|
||||
runBuildTool(
|
||||
"esbuild",
|
||||
"esbuild",
|
||||
[
|
||||
COMPRESSION_WORKER_SRC,
|
||||
"--bundle",
|
||||
"--platform=node",
|
||||
"--packages=external",
|
||||
"--format=esm",
|
||||
`--outfile=${compressionWorkerDest}`,
|
||||
],
|
||||
{ stdio: "inherit" }
|
||||
);
|
||||
console.log("[colocate-standalone] ✅ compression worker bundled");
|
||||
|
||||
// The call-log worker is always present; scope it to ESM immediately. The
|
||||
// optional LLMLingua worker dir is added below only when its deps are installed.
|
||||
const workerDirs = [dirname(callLogWorkerDest)];
|
||||
const workerDirs = [dirname(callLogWorkerDest), dirname(compressionWorkerDest)];
|
||||
|
||||
if (!hasOptionals) {
|
||||
console.log(
|
||||
|
||||
@@ -45,6 +45,7 @@ export const APP_STAGING_ALLOWED_EXACT_PATHS: string[] = [
|
||||
// LLMLingua ONNX worker — esbuild'd standalone .js spawned via worker_threads
|
||||
// (the Next.js bundler can't trace the computed Worker path). Kept like the MCP server.
|
||||
"open-sse/services/compression/engines/llmlingua/onnxWorker.js",
|
||||
"open-sse/services/compression/compressionWorker.js",
|
||||
"src/lib/usage/callLogArtifactWorker.js",
|
||||
"package.json",
|
||||
"peer-stamp.mjs",
|
||||
@@ -312,13 +313,27 @@ export const PACK_ARTIFACT_NEVER_ALLOWED_SEGMENTS: string[] = ["node_modules"];
|
||||
|
||||
export function findUnexpectedArtifactPaths(
|
||||
filePaths: string[],
|
||||
{ exactPaths = [], prefixPaths = [] }: { exactPaths?: string[]; prefixPaths?: string[] } = {}
|
||||
{
|
||||
exactPaths = [],
|
||||
prefixPaths = [],
|
||||
// #9985: the app-STAGING prune (prepublish Step 10.7) must be able to opt out
|
||||
// of the node_modules segment ban — the standalone server's runtime deps live
|
||||
// under dist/node_modules and Turbopack-hashed dirs (.build/next/node_modules/
|
||||
// sql.js-*/dist/sql-wasm.wasm, transformers ort-wasm). Pruning them 500'd every
|
||||
// DB-backed route in packaged boots while /api/monitoring/health stayed green.
|
||||
// The PUBLISH gate (validate-pack-artifact) keeps the strict default.
|
||||
neverAllowedSegments = PACK_ARTIFACT_NEVER_ALLOWED_SEGMENTS,
|
||||
}: {
|
||||
exactPaths?: string[];
|
||||
prefixPaths?: string[];
|
||||
neverAllowedSegments?: string[];
|
||||
} = {}
|
||||
): string[] {
|
||||
const normalizedExact = new Set(exactPaths.map(normalizeArtifactPath));
|
||||
const normalizedPrefixes = prefixPaths.map(normalizeArtifactPath);
|
||||
|
||||
const hasForbiddenSegment = (filePath: string): boolean =>
|
||||
filePath.split("/").some((segment) => PACK_ARTIFACT_NEVER_ALLOWED_SEGMENTS.includes(segment));
|
||||
filePath.split("/").some((segment) => neverAllowedSegments.includes(segment));
|
||||
|
||||
return filePaths
|
||||
.map(normalizeArtifactPath)
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { existsSync, lstatSync, readdirSync, rmSync } from "node:fs";
|
||||
import { existsSync, lstatSync, mkdirSync, readdirSync, rmSync } from "node:fs";
|
||||
import { basename, dirname, join, relative } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { assembleStandalone } from "./assembleStandalone.mjs";
|
||||
import { assertSqlitePrebuildExists } from "./electronRebuildPlan.mjs";
|
||||
import { pruneElectronRuntimeDocs } from "./electronRuntimeDocs.mjs";
|
||||
import { stageOptionalPacks } from "./optionalPackStaging.mjs";
|
||||
import { runBuildTool } from "./buildToolRunner.mjs";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
@@ -169,6 +170,27 @@ assembleStandalone({
|
||||
// app they would point at the build machine's absolute paths and break on install.
|
||||
materializeSymlinks: true,
|
||||
});
|
||||
const compressionWorkerDest = join(
|
||||
ELECTRON_STANDALONE_DIR,
|
||||
"open-sse",
|
||||
"services",
|
||||
"compression",
|
||||
"compressionWorker.js"
|
||||
);
|
||||
mkdirSync(dirname(compressionWorkerDest), { recursive: true });
|
||||
runBuildTool(
|
||||
"esbuild",
|
||||
"esbuild",
|
||||
[
|
||||
join(ROOT, "open-sse", "services", "compression", "compressionWorker.ts"),
|
||||
"--bundle",
|
||||
"--platform=node",
|
||||
"--packages=external",
|
||||
"--format=esm",
|
||||
`--outfile=${compressionWorkerDest}`,
|
||||
],
|
||||
{ stdio: "inherit" }
|
||||
);
|
||||
|
||||
const docsPrune = pruneElectronRuntimeDocs(ELECTRON_STANDALONE_DIR);
|
||||
if (docsPrune.removedFiles > 0) {
|
||||
|
||||
@@ -407,6 +407,40 @@ if (existsSync(llmWorkerSrc)) {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Step 8.6b: Bundle synchronous compression worker ──────────────────
|
||||
const compressionWorkerSrc = join(
|
||||
ROOT,
|
||||
"open-sse",
|
||||
"services",
|
||||
"compression",
|
||||
"compressionWorker.ts"
|
||||
);
|
||||
const compressionWorkerDest = join(
|
||||
DIST_DIR,
|
||||
"open-sse",
|
||||
"services",
|
||||
"compression",
|
||||
"compressionWorker.js"
|
||||
);
|
||||
if (!existsSync(compressionWorkerSrc)) {
|
||||
throw new Error("Required compression worker source is missing");
|
||||
}
|
||||
console.log(" 🔨 Bundling compression worker...");
|
||||
mkdirSync(dirname(compressionWorkerDest), { recursive: true });
|
||||
runBuildTool(
|
||||
"esbuild",
|
||||
"esbuild",
|
||||
[
|
||||
"open-sse/services/compression/compressionWorker.ts",
|
||||
"--bundle",
|
||||
"--platform=node",
|
||||
"--packages=external",
|
||||
"--format=esm",
|
||||
"--outfile=dist/open-sse/services/compression/compressionWorker.js",
|
||||
],
|
||||
{ cwd: ROOT, stdio: "inherit" }
|
||||
);
|
||||
|
||||
// ── Step 8.7: Bundle CLI Entrypoint ──────────────────────────
|
||||
const cliSrcFile = join(ROOT, "bin", "omniroute.ts");
|
||||
const cliDestFile = join(ROOT, "bin", "omniroute.mjs");
|
||||
@@ -639,10 +673,15 @@ for (const relativePath of APP_STAGING_REMOVAL_PATHS) {
|
||||
}
|
||||
|
||||
// ── Step 10.7: Prune any staged dist/ file outside the allowed runtime set ──
|
||||
// #9985: neverAllowedSegments is EMPTY here on purpose — unlike the publish
|
||||
// tarball gate, the staged dist/ legitimately contains node_modules (the
|
||||
// standalone server's runtime deps, including Turbopack-hashed packages whose
|
||||
// wasm files DB init requires). The allowlist prefixes above are the contract.
|
||||
const stagedFiles = walkFiles(DIST_DIR);
|
||||
const unexpectedStagedFiles = findUnexpectedArtifactPaths(stagedFiles, {
|
||||
exactPaths: APP_STAGING_ALLOWED_EXACT_PATHS,
|
||||
prefixPaths: APP_STAGING_ALLOWED_PATH_PREFIXES,
|
||||
neverAllowedSegments: [],
|
||||
});
|
||||
|
||||
if (unexpectedStagedFiles.length > 0) {
|
||||
@@ -657,6 +696,7 @@ if (unexpectedStagedFiles.length > 0) {
|
||||
const remainingUnexpectedFiles = findUnexpectedArtifactPaths(walkFiles(DIST_DIR), {
|
||||
exactPaths: APP_STAGING_ALLOWED_EXACT_PATHS,
|
||||
prefixPaths: APP_STAGING_ALLOWED_PATH_PREFIXES,
|
||||
neverAllowedSegments: [],
|
||||
});
|
||||
|
||||
if (remainingUnexpectedFiles.length > 0) {
|
||||
|
||||
@@ -90,13 +90,30 @@ export function baselineValue(metric, root = ROOT) {
|
||||
}
|
||||
}
|
||||
|
||||
// A line that is unambiguously a PASS. Test reporters print the file name on BOTH the
|
||||
// pass and the fail line, so a green line for a file whose NAME contains "fail"
|
||||
// (fail-fast-*.test.ts, failover-*.test.ts) must never be offered as a failure cause.
|
||||
const GREEN_LINE_RE = /^[✓✔√]/;
|
||||
|
||||
// Markers that are only meaningful at the START of a line: "FAIL" also occurs inside test
|
||||
// FILE NAMES and inside summary prose ("Test Files 1 failed"), so matching it anywhere —
|
||||
// and case-insensitively — reports a PASSING file as the cause of the red.
|
||||
const LINE_START_FAILURE_RE = /^(?:[✖✗×]|FAIL\b|not ok\b|REGRESS)/;
|
||||
|
||||
// Markers that are unambiguous ANYWHERE in the line: tsc and Node emit them mid-line
|
||||
// ("src/x.ts(10,5): error TS2322: ..."), so these stay unanchored. They are matched
|
||||
// case-SENSITIVELY because that is how the emitting tools actually spell them.
|
||||
const INLINE_FAILURE_RE = /\berror TS\d+\b|\bAssertionError\b|\bError:|\bREGRESS/;
|
||||
|
||||
/** Best-effort "first meaningful failure line" from captured command output. */
|
||||
export function firstFailureLine(out) {
|
||||
const lines = String(out || "")
|
||||
.split("\n")
|
||||
.map((l) => l.trim())
|
||||
.filter(Boolean);
|
||||
const hit = lines.find((l) => /✖|✗|not ok|AssertionError|error TS|FAIL|Error:|REGRESS/i.test(l));
|
||||
const hit = lines.find(
|
||||
(l) => !GREEN_LINE_RE.test(l) && (LINE_START_FAILURE_RE.test(l) || INLINE_FAILURE_RE.test(l))
|
||||
);
|
||||
return (hit || lines[lines.length - 1] || "failed").slice(0, 200);
|
||||
}
|
||||
|
||||
@@ -232,6 +249,36 @@ export function fullCiTimeoutFor(gateId) {
|
||||
return FULL_CI_TIMEOUT_OVERRIDES_MS[gateId] ?? FULL_CI_DEFAULT_TIMEOUT_MS;
|
||||
}
|
||||
|
||||
// ci.yml gate scripts whose result the CURATED pass already records under a DIFFERENT id.
|
||||
// Without this map the --full-ci pass re-records them unconditionally as kind:"hard" while
|
||||
// the curated pass recorded them as kind:"drift", and the SAME gate is printed in BOTH
|
||||
// verdict buckets of one report (file-size / compression-budget appeared as a hard failure
|
||||
// and as drift simultaneously in the #9985 verdict).
|
||||
export const FULL_CI_CURATED_ALIASES = {
|
||||
lint: "lint-errors",
|
||||
"check:workflows": "workflow-lint",
|
||||
"check:complexity-ratchets": "complexity",
|
||||
};
|
||||
|
||||
/** Curated-pass id equivalent to a ci.yml gate script id ("check:file-size" -> "file-size"). */
|
||||
export function curatedEquivalentId(scriptId) {
|
||||
const id = String(scriptId || "");
|
||||
if (Object.hasOwn(FULL_CI_CURATED_ALIASES, id)) return FULL_CI_CURATED_ALIASES[id];
|
||||
return id.startsWith("check:") ? id.slice("check:".length) : id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bucket a --full-ci gate must be reported under: the classification the curated pass already
|
||||
* gave the equivalent gate, else "hard" (the --full-ci default for gates the curated list does
|
||||
* not cover). This only changes WHICH BUCKET a result is printed in — it never changes whether
|
||||
* a gate runs, nor whether it passed.
|
||||
*/
|
||||
export function fullCiKindFor(scriptId, results) {
|
||||
const equivalent = curatedEquivalentId(scriptId);
|
||||
const curated = (results || []).find((r) => r.id === scriptId || r.id === equivalent);
|
||||
return curated?.kind ?? "hard";
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a ci.yml text and return the ordered, de-duplicated list of gate commands to run.
|
||||
* Each entry: { id, job, args:["run", <script>, ...("--" + args)], env }.
|
||||
@@ -716,7 +763,10 @@ async function main() {
|
||||
record({
|
||||
id: g.id,
|
||||
label: `ci.yml:${g.job} → npm ${g.args.join(" ")}`,
|
||||
kind: "hard",
|
||||
// Respect the curated classification when the curated pass already ran an equivalent
|
||||
// gate under a different id — otherwise the same ratchet is reported as a HARD failure
|
||||
// here AND as drift above, in one self-contradicting verdict.
|
||||
kind: fullCiKindFor(g.id, results),
|
||||
ok: code === 0,
|
||||
detail: code === 0 ? "pass" : firstFailureLine(out),
|
||||
});
|
||||
|
||||
@@ -75,76 +75,3 @@ omniroute mcp call <tool> [argsJson]
|
||||
```bash
|
||||
omniroute mcp scopes
|
||||
```
|
||||
|
||||
### `mcp tools`
|
||||
|
||||
**Example:**
|
||||
|
||||
```bash
|
||||
omniroute mcp tools
|
||||
```
|
||||
|
||||
### `mcp list`
|
||||
|
||||
**Flags:**
|
||||
|
||||
- `--scope <s>`
|
||||
|
||||
**Example:**
|
||||
|
||||
```bash
|
||||
omniroute mcp list
|
||||
```
|
||||
|
||||
### `mcp info <name>`
|
||||
|
||||
**Example:**
|
||||
|
||||
```bash
|
||||
omniroute mcp info <name>
|
||||
```
|
||||
|
||||
### `mcp schema <name>`
|
||||
|
||||
**Flags:**
|
||||
|
||||
- `--io <kind>`
|
||||
|
||||
**Example:**
|
||||
|
||||
```bash
|
||||
omniroute mcp schema <name>
|
||||
```
|
||||
|
||||
### `mcp audit`
|
||||
|
||||
**Example:**
|
||||
|
||||
```bash
|
||||
omniroute mcp audit
|
||||
```
|
||||
|
||||
### `mcp tail`
|
||||
|
||||
**Flags:**
|
||||
|
||||
- `--follow`
|
||||
- `--limit <n>`
|
||||
|
||||
**Example:**
|
||||
|
||||
```bash
|
||||
omniroute mcp tail
|
||||
```
|
||||
|
||||
### `mcp stats`
|
||||
|
||||
**Flags:**
|
||||
|
||||
- `--period <p>`
|
||||
|
||||
**Example:**
|
||||
|
||||
```bash
|
||||
omniroute mcp stats
|
||||
```
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useCallback, useSyncExternalStore } from "react";
|
||||
import Link from "next/link";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
@@ -10,36 +10,51 @@ type FirstRunReadinessCardProps = {
|
||||
setupComplete: boolean;
|
||||
};
|
||||
|
||||
// #9985: dismissal lives in localStorage, read via useSyncExternalStore — keeps
|
||||
// the component free of setState-in-effect cascades and hydration-safe (server
|
||||
// snapshot treats the card as dismissed; the client corrects after hydration).
|
||||
const readinessListeners = new Set<() => void>();
|
||||
|
||||
function subscribeReadiness(onStoreChange: () => void): () => void {
|
||||
readinessListeners.add(onStoreChange);
|
||||
window.addEventListener("storage", onStoreChange);
|
||||
return () => {
|
||||
readinessListeners.delete(onStoreChange);
|
||||
window.removeEventListener("storage", onStoreChange);
|
||||
};
|
||||
}
|
||||
|
||||
function isReadinessDismissed(): boolean {
|
||||
try {
|
||||
return localStorage.getItem(DISMISS_STORAGE_KEY) === "true";
|
||||
} catch {
|
||||
// Storage unavailable (private mode etc.) — never show the nagging card.
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
function getServerSnapshot(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Soft entry path for first-run users. Replaces the hard redirect to
|
||||
* /dashboard/onboarding so returning users can dismiss and stay on Home.
|
||||
*/
|
||||
export default function FirstRunReadinessCard({ setupComplete }: FirstRunReadinessCardProps) {
|
||||
const t = useTranslations("home");
|
||||
const [visible, setVisible] = useState(false);
|
||||
const dismissed = useSyncExternalStore(subscribeReadiness, isReadinessDismissed, getServerSnapshot);
|
||||
|
||||
useEffect(() => {
|
||||
if (setupComplete) {
|
||||
setVisible(false);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
setVisible(!localStorage.getItem(DISMISS_STORAGE_KEY));
|
||||
} catch {
|
||||
setVisible(true);
|
||||
}
|
||||
}, [setupComplete]);
|
||||
|
||||
if (!visible || setupComplete) return null;
|
||||
|
||||
const dismiss = () => {
|
||||
const dismiss = useCallback(() => {
|
||||
try {
|
||||
localStorage.setItem(DISMISS_STORAGE_KEY, "true");
|
||||
} catch {
|
||||
// ignore storage failures; still hide for this session
|
||||
}
|
||||
setVisible(false);
|
||||
};
|
||||
for (const listener of readinessListeners) listener();
|
||||
}, []);
|
||||
|
||||
if (setupComplete || dismissed) return null;
|
||||
|
||||
const steps = [
|
||||
t("readinessStep1"),
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useMemo, useCallback } from "react";
|
||||
import Link from "next/link";
|
||||
import { Card, Button, Input, Modal, CardSkeleton, SegmentedControl } from "@/shared/components";
|
||||
import Toggle from "@/shared/components/Toggle";
|
||||
import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard";
|
||||
import { isPublicDisplayBaseUrl, useDisplayBaseUrl } from "@/shared/hooks";
|
||||
import { AI_PROVIDERS, getProviderByAlias } from "@/shared/constants/providers";
|
||||
import { getProviderDisplayName } from "@/lib/display/names";
|
||||
import { useTranslations } from "next-intl";
|
||||
import A2ADashboardPage from "./components/A2ADashboard";
|
||||
import McpDashboardPage from "./components/MCPDashboard";
|
||||
@@ -138,7 +135,6 @@ export default function APIPageClient({ machineId }: Readonly<APIPageClientProps
|
||||
// Endpoints / models state
|
||||
const [allModels, setAllModels] = useState([]);
|
||||
const [modelsLoading, setModelsLoading] = useState(true);
|
||||
const [expandedEndpoint, setExpandedEndpoint] = useState(null);
|
||||
|
||||
// Cloud sync state
|
||||
const [cloudEnabled, setCloudEnabled] = useState(false);
|
||||
@@ -151,8 +147,8 @@ export default function APIPageClient({ machineId }: Readonly<APIPageClientProps
|
||||
const [selectedProvider, setSelectedProvider] = useState(null); // for provider models popup
|
||||
const [cloudBaseUrl, setCloudBaseUrl] = useState(BUILD_TIME_CLOUD_URL); // dynamic cloud URL from API response
|
||||
const [cloudConfigured, setCloudConfigured] = useState(Boolean(BUILD_TIME_CLOUD_URL));
|
||||
const [mcpStatus, setMcpStatus] = useState<any>(null);
|
||||
const [a2aStatus, setA2aStatus] = useState<any>(null);
|
||||
const [_mcpStatus, setMcpStatus] = useState<any>(null);
|
||||
const [_a2aStatus, setA2aStatus] = useState<any>(null);
|
||||
const [searchProviders, setSearchProviders] = useState<any[]>([]);
|
||||
const [cloudflaredStatus, setCloudflaredStatus] = useState<CloudflaredTunnelStatus | null>(null);
|
||||
const [cloudflaredBusy, setCloudflaredBusy] = useState(false);
|
||||
@@ -1132,10 +1128,6 @@ export default function APIPageClient({ machineId }: Readonly<APIPageClientProps
|
||||
showNgrokTunnel && ngrokStatus?.running,
|
||||
].filter(Boolean).length;
|
||||
|
||||
const mcpOnline = Boolean(mcpStatus?.online);
|
||||
const a2aOnline = a2aStatus?.status === "ok";
|
||||
const mcpToolCount = Number(mcpStatus?.heartbeat?.toolCount || 0);
|
||||
const a2aActiveStreams = Number(a2aStatus?.tasks?.activeStreams || 0);
|
||||
const cloudflaredPhase = cloudflaredStatus?.phase || "not_installed";
|
||||
const cloudflaredPhaseMeta: Record<CloudflaredTunnelPhase, { label: string; className: string }> =
|
||||
{
|
||||
@@ -1169,10 +1161,6 @@ export default function APIPageClient({ machineId }: Readonly<APIPageClientProps
|
||||
: cloudflaredStatus?.installed
|
||||
? translateOrFallback("cloudflaredEnable", "Enable Tunnel")
|
||||
: translateOrFallback("cloudflaredInstallAndEnable", "Install & Enable");
|
||||
const cloudflaredUrlNotice = translateOrFallback(
|
||||
"cloudflaredUrlNotice",
|
||||
"Creates a temporary Cloudflare Quick Tunnel. The URL changes after every restart."
|
||||
);
|
||||
const tailscalePhase = tailscaleStatus?.phase || "not_installed";
|
||||
const tailscalePhaseMeta: Record<TailscaleTunnelPhase, { label: string; className: string }> = {
|
||||
running: {
|
||||
@@ -2498,137 +2486,3 @@ function EndpointCard({
|
||||
);
|
||||
}
|
||||
|
||||
function EndpointSection({
|
||||
icon,
|
||||
iconColor,
|
||||
iconBg,
|
||||
title,
|
||||
path,
|
||||
description,
|
||||
models,
|
||||
expanded,
|
||||
onToggle,
|
||||
copy,
|
||||
copied,
|
||||
baseUrl,
|
||||
modelsLoading = false,
|
||||
}: Readonly<{
|
||||
icon: string;
|
||||
iconColor: string;
|
||||
iconBg: string;
|
||||
title: string;
|
||||
path: string;
|
||||
description: string;
|
||||
models: EndpointModelSummary[];
|
||||
expanded: boolean;
|
||||
onToggle: () => void;
|
||||
copy: CopyHandler;
|
||||
copied?: string | null;
|
||||
baseUrl: string;
|
||||
modelsLoading?: boolean;
|
||||
}>) {
|
||||
const t = useTranslations("endpoint");
|
||||
const grouped = useMemo(() => {
|
||||
const map = {};
|
||||
for (const m of models) {
|
||||
const owner = m.owned_by || "unknown";
|
||||
if (!map[owner]) map[owner] = [];
|
||||
map[owner].push(m);
|
||||
}
|
||||
return Object.entries(map).sort((a: any, b: any) => b[1].length - a[1].length);
|
||||
}, [models]);
|
||||
|
||||
const resolveProvider = (id) => AI_PROVIDERS[id] || getProviderByAlias(id);
|
||||
const providerColor = (id) => resolveProvider(id)?.color || "#888";
|
||||
const providerName = (id) => getProviderDisplayName(id, resolveProvider(id));
|
||||
const copyId = `endpoint_${path}`;
|
||||
|
||||
return (
|
||||
<div className="border border-border rounded-lg overflow-hidden">
|
||||
{/* Header (always visible) */}
|
||||
<button
|
||||
onClick={onToggle}
|
||||
className="w-full flex items-center gap-3 p-4 hover:bg-surface/50 transition-colors text-left"
|
||||
>
|
||||
<div className={`flex items-center justify-center size-10 rounded-lg ${iconBg} shrink-0`}>
|
||||
<span className={`material-symbols-outlined text-xl ${iconColor}`}>{icon}</span>
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-semibold text-sm">{title}</span>
|
||||
<span className="text-xs px-2 py-0.5 rounded-full bg-surface text-text-muted font-medium">
|
||||
{modelsLoading ? "..." : t("modelsCount", { count: models.length })}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-text-muted mt-0.5">{description}</p>
|
||||
</div>
|
||||
<span
|
||||
className={`material-symbols-outlined text-text-muted text-lg transition-transform ${expanded ? "rotate-180" : ""}`}
|
||||
>
|
||||
expand_more
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{/* Expanded content */}
|
||||
{expanded && (
|
||||
<div className="border-t border-border px-4 pb-4">
|
||||
{/* Endpoint path + copy */}
|
||||
<div className="flex items-center gap-2 mt-3 mb-3">
|
||||
<code className="flex-1 text-xs font-mono text-text-muted bg-surface/80 px-3 py-1.5 rounded-lg truncate">
|
||||
{baseUrl.replace(/\/v1$/, "")}
|
||||
{path}
|
||||
</code>
|
||||
<button
|
||||
onClick={() => copy(`${baseUrl.replace(/\/v1$/, "")}${path}`, copyId)}
|
||||
className="p-1.5 hover:bg-surface rounded-lg text-text-muted hover:text-primary transition-colors shrink-0"
|
||||
>
|
||||
<span className="material-symbols-outlined text-[16px]">
|
||||
{copied === copyId ? "check" : "content_copy"}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Models grouped by provider */}
|
||||
{modelsLoading ? (
|
||||
<div className="flex items-center gap-2 rounded-lg border border-border/70 bg-surface/40 px-3 py-2 text-xs text-text-muted">
|
||||
<span className="material-symbols-outlined animate-spin text-sm">
|
||||
progress_activity
|
||||
</span>
|
||||
<span>{t("loadingModels")}</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col gap-2">
|
||||
{grouped.map(([providerId, providerModels]) => (
|
||||
<div key={providerId}>
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<div
|
||||
className="size-2.5 rounded-full shrink-0"
|
||||
style={{ backgroundColor: providerColor(providerId) }}
|
||||
/>
|
||||
<span className="text-xs font-semibold text-text-main">
|
||||
{providerName(providerId)}
|
||||
</span>
|
||||
<span className="text-xs text-text-muted">
|
||||
({(providerModels as any).length})
|
||||
</span>
|
||||
</div>
|
||||
<div className="ml-5 flex flex-wrap gap-1.5">
|
||||
{(providerModels as any).map((m) => (
|
||||
<span
|
||||
key={m.id}
|
||||
className="text-xs px-2 py-0.5 rounded-md bg-surface/80 text-text-muted font-mono"
|
||||
title={m.id}
|
||||
>
|
||||
{m.root || m.id.split("/").pop()}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,10 @@ type AdaptaTutorialModalProps = {
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
// The Adapta CTA href points at https://link.omniroute.online/adapta (our own
|
||||
// shortener, the `adapta` slug) so the click lands in our Kutt metrics. The visible
|
||||
// link text intentionally stays the real domain (agent.adapta.one/agentic-chat) so
|
||||
// users still see where they are going.
|
||||
export function AdaptaTutorialModal({ isOpen, onClose }: AdaptaTutorialModalProps) {
|
||||
const t = useTranslations("providers.adaptaTutorial");
|
||||
|
||||
@@ -29,7 +33,7 @@ export function AdaptaTutorialModal({ isOpen, onClose }: AdaptaTutorialModalProp
|
||||
<p className="text-text-muted mt-0.5">
|
||||
{t("step1DescPrefix")}{" "}
|
||||
<a
|
||||
href="https://agent.adapta.one/agentic-chat"
|
||||
href="https://link.omniroute.online/adapta"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline text-primary"
|
||||
|
||||
@@ -290,6 +290,13 @@ export default function EditConnectionModal({
|
||||
connection.providerSpecificData?.quotaPerUnit != null
|
||||
? String(connection.providerSpecificData.quotaPerUnit)
|
||||
: "";
|
||||
// Modal-open form initialization from the loaded connection (sync with an
|
||||
// external system on `isOpen`); remounting the 30+ field form per
|
||||
// connection id is a behavior-risking restructure out of scope here
|
||||
// (#11251 follow-up, #9985).
|
||||
// NOTE: no react-hooks/set-state-in-effect suppression needed — the rule
|
||||
// only fires on unconditional synchronous setState, and this one is
|
||||
// guarded by the isOpen/connection condition above.
|
||||
setFormData({
|
||||
name: connection.name || "",
|
||||
priority: connection.priority || 1,
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { Badge, Button, Card } from "@/shared/components";
|
||||
import type {
|
||||
CliproxyAccountHealth,
|
||||
CliproxyAccountHealthResult,
|
||||
} from "@/lib/services/cliproxyAccountHealth";
|
||||
|
||||
const STATE_LABELS: Record<CliproxyAccountHealthResult["state"], string> = {
|
||||
ready: "Account health",
|
||||
disabled: "CLIProxyAPI is not installed",
|
||||
missing_key: "Management key is not configured",
|
||||
unreachable: "Management API is unreachable",
|
||||
unauthorized: "Management key was rejected",
|
||||
unsupported: "This CLIProxyAPI version does not expose account health",
|
||||
invalid_response: "Management API returned an unsupported response",
|
||||
};
|
||||
|
||||
function AccountRow({ account }: { account: CliproxyAccountHealth }) {
|
||||
const state = account.disabled ? "Disabled" : account.unavailable ? "Unavailable" : account.status;
|
||||
return (
|
||||
<li className="flex flex-wrap items-center justify-between gap-3 border-t border-border py-3 first:border-t-0">
|
||||
<div className="min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="truncate font-medium text-text-main">
|
||||
{account.label || account.authIndex}
|
||||
</span>
|
||||
<Badge variant={account.disabled || account.unavailable ? "warning" : "success"}>
|
||||
{state || "Unknown"}
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="mt-1 text-xs text-text-muted">
|
||||
{[account.provider || account.type, account.label ? account.authIndex : ""]
|
||||
.filter(Boolean)
|
||||
.join(" · ")}
|
||||
</p>
|
||||
</div>
|
||||
<div className="text-right text-xs text-text-muted">
|
||||
<div>{account.success.toLocaleString()} succeeded</div>
|
||||
<div>{account.failed.toLocaleString()} failed</div>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
export function CliproxyAccountHealthCard() {
|
||||
const [result, setResult] = useState<CliproxyAccountHealthResult | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await fetch("/api/services/cliproxy/accounts", { cache: "no-store" });
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||
setResult(await response.json());
|
||||
} catch {
|
||||
setResult({ state: "unreachable", accounts: [], version: null });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
void load();
|
||||
}, [load]);
|
||||
|
||||
return (
|
||||
<Card
|
||||
title="CLIProxyAPI accounts"
|
||||
subtitle="Read-only status from the authenticated management API"
|
||||
action={
|
||||
<Button variant="secondary" size="sm" onClick={() => void load()} loading={loading}>
|
||||
Refresh
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
{result?.state === "ready" ? (
|
||||
result.accounts.length > 0 ? (
|
||||
<ul aria-label="CLIProxyAPI account health">
|
||||
{result.accounts.map((account) => (
|
||||
<AccountRow key={account.authIndex} account={account} />
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<p className="text-sm text-text-muted">No CLIProxyAPI accounts found.</p>
|
||||
)
|
||||
) : (
|
||||
<p className="text-sm text-text-muted">
|
||||
{loading && !result ? "Loading account health…" : STATE_LABELS[result?.state ?? "unreachable"]}
|
||||
</p>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import { AutoStartToggle } from "../components/AutoStartToggle";
|
||||
import { AutoRestartAdoptedToggle } from "../components/AutoRestartAdoptedToggle";
|
||||
import { CliproxyConnectionPanel } from "../components/CliproxyConnectionPanel";
|
||||
import { CliproxyProviderExposureCard } from "../components/CliproxyProviderExposureCard";
|
||||
import { CliproxyAccountHealthCard } from "../components/CliproxyAccountHealthCard";
|
||||
|
||||
const NAME = "cliproxy";
|
||||
|
||||
@@ -19,6 +20,7 @@ export function CliproxyServiceTab() {
|
||||
<AutoStartToggle name={NAME} />
|
||||
<AutoRestartAdoptedToggle name={NAME} />
|
||||
<CliproxyConnectionPanel />
|
||||
<CliproxyAccountHealthCard />
|
||||
<CliproxyProviderExposureCard />
|
||||
<CliproxyModelMappingEditor />
|
||||
<ServiceLogsPanel name={NAME} />
|
||||
|
||||
@@ -521,6 +521,7 @@ export default function SidebarTab() {
|
||||
|
||||
const presetLabels: Record<SidebarPresetId, string> = {
|
||||
all: getSettingsLabel("presetAll", "All"),
|
||||
essentials: getSettingsLabel("presetEssentials", "Essentials"),
|
||||
minimal: getSettingsLabel("presetMinimal", "Minimal"),
|
||||
developer: getSettingsLabel("presetDeveloper", "Developer"),
|
||||
admin: getSettingsLabel("presetAdmin", "Admin"),
|
||||
@@ -528,6 +529,10 @@ export default function SidebarTab() {
|
||||
|
||||
const presetDescriptions: Record<SidebarPresetId, string> = {
|
||||
all: getSettingsLabel("presetAllDesc", "Show everything"),
|
||||
essentials: getSettingsLabel(
|
||||
"presetEssentialsDesc",
|
||||
"Beginner path — Advanced tools stay searchable"
|
||||
),
|
||||
minimal: getSettingsLabel("presetMinimalDesc", "Core pages only"),
|
||||
developer: getSettingsLabel("presetDeveloperDesc", "Dev & proxy tools"),
|
||||
admin: getSettingsLabel("presetAdminDesc", "Monitoring & audit"),
|
||||
|
||||
@@ -15,7 +15,15 @@ import { HistoricSessionBanner } from "./components/session/HistoricSessionBanne
|
||||
|
||||
const BUFFER_MAX = 1000;
|
||||
|
||||
export function TrafficInspectorPageClient() {
|
||||
export function TrafficInspectorPageClient({
|
||||
title,
|
||||
subtitle,
|
||||
purpose,
|
||||
}: {
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
purpose?: string;
|
||||
} = {}) {
|
||||
const [containerHeight, setContainerHeight] = useState(600);
|
||||
const listContainerRef = useRef<HTMLDivElement | null>(null);
|
||||
const [selectedRequest, setSelectedRequest] = useState<InterceptedRequest | null>(null);
|
||||
@@ -91,6 +99,18 @@ export function TrafficInspectorPageClient() {
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full overflow-hidden">
|
||||
{title && (
|
||||
<div className="shrink-0 px-4 pt-4 pb-2">
|
||||
<h1 className="text-2xl font-bold text-text-main">{title}</h1>
|
||||
{subtitle && (
|
||||
<p className="text-sm text-text-muted mt-1 max-w-2xl">{subtitle}</p>
|
||||
)}
|
||||
{purpose && (
|
||||
<p className="text-xs text-text-muted mt-2 max-w-2xl italic">{purpose}</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Capture modes toolbar */}
|
||||
<div className="shrink-0 px-4 pt-4 pb-2">
|
||||
<CaptureModesToolbar customHostCount={0} />
|
||||
|
||||
@@ -9,6 +9,7 @@ export async function generateMetadata() {
|
||||
};
|
||||
}
|
||||
|
||||
export default function TrafficInspectorPage() {
|
||||
return <TrafficInspectorPageClient />;
|
||||
export default async function TrafficInspectorPage() {
|
||||
const t = await getTranslations("sidebar");
|
||||
return <TrafficInspectorPageClient title={t("trafficInspector")} subtitle={t("trafficInspectorSubtitle")} purpose={t("trafficInspectorPurpose")} />;
|
||||
}
|
||||
|
||||
@@ -10,15 +10,13 @@
|
||||
* Auth: Bearer token via Authorization header
|
||||
*/
|
||||
|
||||
import { timingSafeEqual } from "node:crypto";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { getTaskManager } from "@/lib/a2a/taskManager";
|
||||
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";
|
||||
import { authenticateA2ARequest, resolveA2AOwner } from "@/lib/a2a/authenticate";
|
||||
|
||||
// ============ A2A v1.0 ↔ v0.3 compatibility layer ============
|
||||
// A2A 1.0 renamed the JSON-RPC methods (message/send → SendMessage,
|
||||
@@ -55,7 +53,7 @@ function buildV1Task(
|
||||
? result.artifacts
|
||||
.map((a) =>
|
||||
a && typeof a === "object" && typeof (a as { content?: unknown }).content === "string"
|
||||
? ((a as { content: string }).content)
|
||||
? (a as { content: string }).content
|
||||
: ""
|
||||
)
|
||||
.filter((s) => s.length > 0)
|
||||
@@ -124,39 +122,13 @@ function toMessageArray(raw: unknown): A2AMessage[] | null {
|
||||
|
||||
// ============ Auth ============
|
||||
|
||||
/**
|
||||
* Constant-time comparison of the presented bearer token against the configured
|
||||
* key. A plain `===` short-circuits on the first differing byte, leaking the
|
||||
* length of the shared prefix through response timing; `timingSafeEqual` does
|
||||
* not. It requires equal-length buffers, so mismatched lengths are rejected up
|
||||
* front (the length itself is not secret).
|
||||
*/
|
||||
function tokensMatch(provided: string, expected: string): boolean {
|
||||
const a = Buffer.from(provided);
|
||||
const b = Buffer.from(expected);
|
||||
if (a.length !== b.length) return false;
|
||||
return timingSafeEqual(a, b);
|
||||
}
|
||||
|
||||
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 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;
|
||||
// (GHSA-v54m-6rm3-p565). The shared helper applies the same posture on both
|
||||
// the JSON-RPC and the REST task surfaces (GHSA-jcm5-6wpp-wjj8).
|
||||
return authenticateA2ARequest(req);
|
||||
}
|
||||
|
||||
// ============ JSON-RPC Helpers ============
|
||||
@@ -213,6 +185,9 @@ export async function POST(req: NextRequest) {
|
||||
if (disabledResponse) return disabledResponse;
|
||||
|
||||
const tm = getTaskManager();
|
||||
// GHSA-jcm5-6wpp-wjj8: scope every task read/mutation below to the caller's
|
||||
// owner id (hashed API key; undefined under the keyless local-first posture).
|
||||
const callerOwner = resolveA2AOwner(req);
|
||||
|
||||
// A2A 1.0 method-name compatibility (SendMessage → message/send, etc.)
|
||||
const isV1Method = method in V1_METHOD_ALIASES;
|
||||
@@ -236,7 +211,7 @@ export async function POST(req: NextRequest) {
|
||||
return jsonRpcError(id, -32601, `Unknown skill: ${skill}`);
|
||||
}
|
||||
|
||||
const task = tm.createTask({ skill, messages, metadata: params?.metadata });
|
||||
const task = tm.createTask({ skill, messages, metadata: params?.metadata }, callerOwner);
|
||||
try {
|
||||
tm.updateTask(task.id, "working");
|
||||
const result = await handler(task);
|
||||
@@ -302,7 +277,7 @@ export async function POST(req: NextRequest) {
|
||||
return jsonRpcError(id, -32601, `Unknown skill: ${skill}`);
|
||||
}
|
||||
|
||||
const task = tm.createTask({ skill, messages, metadata: params?.metadata });
|
||||
const task = tm.createTask({ skill, messages, metadata: params?.metadata }, callerOwner);
|
||||
tm.updateTask(task.id, "working");
|
||||
|
||||
const stream = createA2AStream(
|
||||
@@ -323,7 +298,7 @@ export async function POST(req: NextRequest) {
|
||||
const taskId = params?.taskId || params?.id;
|
||||
if (!taskId) return jsonRpcError(id, -32602, "Invalid params: taskId required");
|
||||
|
||||
const task = tm.getTask(taskId);
|
||||
const task = tm.getTask(taskId, callerOwner);
|
||||
if (!task) return jsonRpcError(id, -32601, `Task not found: ${taskId}`);
|
||||
|
||||
return jsonRpcResult(id, { task });
|
||||
@@ -335,7 +310,7 @@ export async function POST(req: NextRequest) {
|
||||
if (!taskId) return jsonRpcError(id, -32602, "Invalid params: taskId required");
|
||||
|
||||
try {
|
||||
const task = tm.cancelTask(taskId);
|
||||
const task = tm.cancelTask(taskId, callerOwner);
|
||||
return jsonRpcResult(id, { task: { id: task.id, state: task.state } });
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
|
||||
51
src/app/api/a2a/_auth.ts
Normal file
51
src/app/api/a2a/_auth.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* Shared authorization for the REST A2A task routes (GHSA-jcm5-6wpp-wjj8).
|
||||
*
|
||||
* Dual audience: the dashboard calls these routes with a management session,
|
||||
* A2A clients with an inference API key. Posture matrix:
|
||||
*
|
||||
* - REQUIRE_API_KEY=true: a valid OmniRoute key is mandatory (the same
|
||||
* posture the /v1 inference plane enforces); a management session also
|
||||
* passes (dashboard), via alwaysRequireAuth so requireLogin=false cannot
|
||||
* bypass it.
|
||||
* - otherwise + requireLogin=true: management session, or a valid key.
|
||||
* - otherwise + requireLogin=false (local-first default): open, by design.
|
||||
*
|
||||
* Callers authenticated by key are owner-scoped — another principal's tasks
|
||||
* answer as if they did not exist. Management/operator view sees all tasks.
|
||||
*/
|
||||
|
||||
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
|
||||
import { extractApiKey, isValidApiKey } from "@/sse/services/auth";
|
||||
import { isRequireApiKeyEnabled } from "@/shared/utils/featureFlags";
|
||||
import { resolveA2AOwner } from "@/lib/a2a/authenticate";
|
||||
|
||||
export interface A2ARestAuth {
|
||||
/** Owner scope for task reads/mutations; undefined = operator view (all tasks). */
|
||||
owner: string | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* NOTE: the failure branch is whatever requireManagementAuth returns — today a
|
||||
* plain `Response` from createErrorResponse(), NOT a NextResponse. Callers must
|
||||
* test with `instanceof Response` (NextResponse extends Response), never
|
||||
* `instanceof NextResponse`, or the 401 silently falls through to the handler.
|
||||
*/
|
||||
export async function authorizeA2ATaskRoute(request: Request): Promise<A2ARestAuth | Response> {
|
||||
const apiKey = extractApiKey(request);
|
||||
|
||||
if (isRequireApiKeyEnabled()) {
|
||||
if (apiKey && (await isValidApiKey(apiKey))) return { owner: resolveA2AOwner(request) };
|
||||
const managementError = await requireManagementAuth(request, {
|
||||
invalidApiKeyStatus: 401,
|
||||
alwaysRequireAuth: true,
|
||||
});
|
||||
if (managementError === null) return { owner: undefined };
|
||||
return managementError;
|
||||
}
|
||||
|
||||
const managementError = await requireManagementAuth(request, { invalidApiKeyStatus: 401 });
|
||||
if (managementError === null) return { owner: undefined };
|
||||
if (apiKey && (await isValidApiKey(apiKey))) return { owner: resolveA2AOwner(request) };
|
||||
return managementError;
|
||||
}
|
||||
@@ -1,14 +1,23 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { getTaskManager } from "@/lib/a2a/taskManager";
|
||||
import { authorizeA2ATaskRoute } from "@/app/api/a2a/_auth";
|
||||
import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/error";
|
||||
|
||||
export async function POST(_request: Request, { params }: { params: Promise<{ id: string }> }) {
|
||||
export async function POST(request: Request, { params }: { params: Promise<{ id: string }> }) {
|
||||
// GHSA-jcm5-6wpp-wjj8: this route had no auth call at all. The owner check
|
||||
// happens inside cancelTask: another principal's task throws the same
|
||||
// "not found" a missing one would (no existence oracle).
|
||||
const auth = await authorizeA2ATaskRoute(request);
|
||||
if (auth instanceof Response) return auth;
|
||||
try {
|
||||
const { id } = await params;
|
||||
const tm = getTaskManager();
|
||||
const task = tm.cancelTask(id);
|
||||
const task = tm.cancelTask(id, auth.owner);
|
||||
return NextResponse.json({ task: { id: task.id, state: task.state } });
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Failed to cancel A2A task";
|
||||
const message = sanitizeErrorMessage(
|
||||
error instanceof Error ? error.message : "Failed to cancel A2A task"
|
||||
);
|
||||
const status = message.includes("not found") ? 404 : 400;
|
||||
return NextResponse.json({ error: message }, { status });
|
||||
}
|
||||
|
||||
@@ -1,17 +1,30 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { getTaskManager } from "@/lib/a2a/taskManager";
|
||||
import { authorizeA2ATaskRoute } from "@/app/api/a2a/_auth";
|
||||
import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/error";
|
||||
|
||||
export async function GET(_request: Request, { params }: { params: Promise<{ id: string }> }) {
|
||||
export async function GET(request: Request, { params }: { params: Promise<{ id: string }> }) {
|
||||
// GHSA-jcm5-6wpp-wjj8: this route had no auth call at all — open regardless
|
||||
// of configuration. Another principal's task answers 404, same as a missing
|
||||
// one, so an IDOR probe cannot tell the two apart.
|
||||
const auth = await authorizeA2ATaskRoute(request);
|
||||
if (auth instanceof Response) return auth;
|
||||
try {
|
||||
const { id } = await params;
|
||||
const tm = getTaskManager();
|
||||
const task = tm.getTask(id);
|
||||
const task = tm.getTask(id, auth.owner);
|
||||
if (!task) {
|
||||
return NextResponse.json({ error: `Task not found: ${id}` }, { status: 404 });
|
||||
}
|
||||
return NextResponse.json({ task });
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Failed to load A2A task";
|
||||
return NextResponse.json({ error: message }, { status: 500 });
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: sanitizeErrorMessage(
|
||||
error instanceof Error ? error.message : "Failed to load A2A task"
|
||||
),
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { NextResponse } from "next/server";
|
||||
import { z } from "zod";
|
||||
|
||||
import { getTaskManager, type TaskState } from "@/lib/a2a/taskManager";
|
||||
import { authorizeA2ATaskRoute } from "@/app/api/a2a/_auth";
|
||||
import { createConductorTask } from "@/lib/conductor/hubProxy";
|
||||
import { getSettings } from "@/lib/db/settings";
|
||||
|
||||
@@ -22,6 +23,11 @@ function parseIntParam(value: string | null, fallback: number): number {
|
||||
}
|
||||
|
||||
export async function GET(request: Request) {
|
||||
// GHSA-jcm5-6wpp-wjj8: the list route had no auth call at all. Management
|
||||
// (or the keyless posture) sees every task; a bare API key must be valid
|
||||
// and is owner-scoped.
|
||||
const auth = await authorizeA2ATaskRoute(request);
|
||||
if (auth instanceof Response) return auth;
|
||||
try {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const stateParam = searchParams.get("state");
|
||||
@@ -36,7 +42,7 @@ export async function GET(request: Request) {
|
||||
|
||||
const tm = getTaskManager();
|
||||
const total = tm.countTasks({ state, skill });
|
||||
const tasks = tm.listTasks({ state, skill, limit, offset });
|
||||
const tasks = tm.listTasks({ state, skill, limit, offset }, auth.owner);
|
||||
|
||||
return NextResponse.json({
|
||||
tasks,
|
||||
@@ -104,7 +110,10 @@ export function authenticateA2A(request: Request): boolean {
|
||||
*/
|
||||
export async function POST(request: Request) {
|
||||
if (!authenticateA2A(request)) {
|
||||
return NextResponse.json({ error: "Unauthorized: missing or invalid API key" }, { status: 401 });
|
||||
return NextResponse.json(
|
||||
{ error: "Unauthorized: missing or invalid API key" },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
const settings = await getSettings();
|
||||
if (settings.a2aEnabled !== true) {
|
||||
@@ -122,12 +131,18 @@ export async function POST(request: Request) {
|
||||
}
|
||||
const parsed = delegationSchema.safeParse(raw);
|
||||
if (!parsed.success) {
|
||||
return NextResponse.json({ error: "Invalid A2A task: provide messages[] (and metadata.conductor)" }, { status: 400 });
|
||||
return NextResponse.json(
|
||||
{ error: "Invalid A2A task: provide messages[] (and metadata.conductor)" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
const { skill, messages, metadata } = parsed.data;
|
||||
if (skill !== "conductor" && !skill.startsWith("conductor-cli-")) {
|
||||
return NextResponse.json(
|
||||
{ error: "Only Conductor fleet skills are delegable here (conductor / conductor-cli-<profile>)" },
|
||||
{
|
||||
error:
|
||||
"Only Conductor fleet skills are delegable here (conductor / conductor-cli-<profile>)",
|
||||
},
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
@@ -138,7 +153,9 @@ export async function POST(request: Request) {
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
const prompt = [...messages].reverse().find((m) => m.role === "user")?.content ?? messages[messages.length - 1].content;
|
||||
const prompt =
|
||||
[...messages].reverse().find((m) => m.role === "user")?.content ??
|
||||
messages[messages.length - 1].content;
|
||||
|
||||
const created = await createConductorTask({
|
||||
repoUrl: conductor.repo.url,
|
||||
|
||||
@@ -439,13 +439,22 @@ type ProviderConnectionLike = {
|
||||
* whose stored `providerSpecificData.profileArn` matches the given ARN.
|
||||
* Returns null when profileArn is undefined/null or no match is found.
|
||||
*
|
||||
* #10815 hardened `findKiroConnectionByIdentity` to require an account-level
|
||||
* identifier (email or clientId) alongside a matching profileArn before
|
||||
* trusting the match — distinct Builder ID accounts (Google/GitHub social
|
||||
* login) can share the same CodeWhisperer profile ARN, and matching on ARN
|
||||
* alone let a second social login silently overwrite the first connection.
|
||||
* `email`/`clientId` here let a caller supply that account identifier; the
|
||||
* real `saveAndRespond()` call sites already do (see below).
|
||||
*
|
||||
* Exported for unit tests (#3615).
|
||||
*/
|
||||
export function findKiroConnectionByProfileArn(
|
||||
connections: ProviderConnectionLike[],
|
||||
profileArn: string | undefined
|
||||
profileArn: string | undefined,
|
||||
accountIdentity?: { email?: string | null; clientId?: string | null }
|
||||
): ProviderConnectionLike | null {
|
||||
return findKiroConnectionByIdentity(connections, { profileArn });
|
||||
return findKiroConnectionByIdentity(connections, { profileArn, ...accountIdentity });
|
||||
}
|
||||
|
||||
// ── Save to OmniRoute DB ──────────────────────────────────────────────────────
|
||||
|
||||
@@ -29,6 +29,7 @@ import { canUpdateProviderApiKey } from "@/shared/providers/webSessionCredential
|
||||
import {
|
||||
refreshConnectionRateLimits,
|
||||
enableRateLimitProtection,
|
||||
disableRateLimitProtection,
|
||||
} from "@/../open-sse/services/rateLimitManager";
|
||||
import {
|
||||
finalizeValidatedChatGptWebCodexSecrets,
|
||||
@@ -342,10 +343,18 @@ export async function PUT(request: Request, { params }: { params: Promise<{ id:
|
||||
|
||||
// If rateLimitOverrides was included in the request, refresh the in-memory
|
||||
// rate limiter state so the change takes effect without a server restart.
|
||||
// Also ensure rate limit protection is active so the limiter is enforced.
|
||||
// Only (re)enable enforcement when rate limit protection is actually
|
||||
// persisted for this connection — this route never lets a caller flip
|
||||
// `rateLimitProtection` itself, so any drift here would silently start
|
||||
// queuing requests through Bottleneck for a connection whose DB row (and
|
||||
// the dashboard toggle reading it) both still say "off" (#11278).
|
||||
if (rateLimitOverrides !== undefined) {
|
||||
refreshConnectionRateLimits(id, updated?.rateLimitOverrides ?? null);
|
||||
enableRateLimitProtection(id);
|
||||
if (updated?.rateLimitProtection === true) {
|
||||
enableRateLimitProtection(id);
|
||||
} else {
|
||||
disableRateLimitProtection(id);
|
||||
}
|
||||
}
|
||||
|
||||
// Hide sensitive fields
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import { getSupervisor, registerSupervisor } from "@/lib/services/registry";
|
||||
import { ServiceSupervisor } from "@/lib/services/ServiceSupervisor";
|
||||
import { resolveSpawnArgs, CLIPROXY_DEFAULT_PORT } from "@/lib/services/installers/cliproxy";
|
||||
import { getOrCreateApiKey } from "@/lib/services/apiKey";
|
||||
|
||||
const TOOL = "cliproxy";
|
||||
const PORT = parseInt(process.env.CLIPROXYAPI_PORT ?? String(CLIPROXY_DEFAULT_PORT), 10);
|
||||
@@ -14,10 +15,11 @@ export async function getOrInitSupervisor(): Promise<ServiceSupervisor> {
|
||||
const existing = getSupervisor(TOOL);
|
||||
if (existing) return existing;
|
||||
|
||||
const managementKey = await getOrCreateApiKey(TOOL);
|
||||
const sup = new ServiceSupervisor({
|
||||
tool: TOOL,
|
||||
port: PORT,
|
||||
spawnArgs: () => resolveSpawnArgs(PORT),
|
||||
spawnArgs: () => resolveSpawnArgs(PORT, managementKey),
|
||||
healthUrl: () => `http://127.0.0.1:${PORT}/v1/models`,
|
||||
healthIntervalMs: 5_000,
|
||||
stopTimeoutMs: 15_000,
|
||||
|
||||
13
src/app/api/services/cliproxy/accounts/route.ts
Normal file
13
src/app/api/services/cliproxy/accounts/route.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { getCliproxyAccountHealth } from "@/lib/services/cliproxyAccountHealth";
|
||||
import { isAuthenticated } from "@/shared/utils/apiAuth";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export async function GET(request: Request): Promise<Response> {
|
||||
if (!(await isAuthenticated(request))) {
|
||||
return Response.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
return Response.json(await getCliproxyAccountHealth(), {
|
||||
headers: { "Cache-Control": "no-store" },
|
||||
});
|
||||
}
|
||||
104
src/app/api/v1/_shared/elevenLabsProxy.ts
Normal file
104
src/app/api/v1/_shared/elevenLabsProxy.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import {
|
||||
clearRecoveredProviderState,
|
||||
getProviderCredentialsWithQuotaPreflight,
|
||||
} from "@/sse/services/auth";
|
||||
import {
|
||||
isAllRateLimitedCredentials,
|
||||
rateLimitedProviderResponse,
|
||||
} from "@/app/api/v1/_shared/rateLimit";
|
||||
import {
|
||||
buildErrorBody,
|
||||
sanitizeErrorMessage,
|
||||
} from "@omniroute/open-sse/utils/error.ts";
|
||||
import { CORS_HEADERS, handleCorsOptions } from "@/shared/utils/cors";
|
||||
|
||||
const ELEVENLABS_API_BASE = "https://api.elevenlabs.io/v1";
|
||||
const ALLOWED_RESPONSE_HEADERS = [
|
||||
"content-type",
|
||||
"content-disposition",
|
||||
"request-id",
|
||||
"retry-after",
|
||||
] as const;
|
||||
|
||||
type ElevenLabsCredentials = {
|
||||
apiKey?: string | null;
|
||||
accessToken?: string | null;
|
||||
allExpired?: boolean;
|
||||
};
|
||||
|
||||
export function elevenLabsOptionsResponse(): Response {
|
||||
return handleCorsOptions();
|
||||
}
|
||||
|
||||
export function isSafeElevenLabsVoiceId(value: string): boolean {
|
||||
return /^[A-Za-z0-9_-]+$/.test(value);
|
||||
}
|
||||
|
||||
function proxyResponseHeaders(upstream: Response): Headers {
|
||||
const headers = new Headers(CORS_HEADERS);
|
||||
for (const name of ALLOWED_RESPONSE_HEADERS) {
|
||||
const value = upstream.headers.get(name);
|
||||
if (value) headers.set(name, value);
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
|
||||
export async function proxyElevenLabsRequest(
|
||||
request: Request,
|
||||
pathname: string,
|
||||
init: Omit<RequestInit, "headers"> = {}
|
||||
): Promise<Response> {
|
||||
const credentials = (await getProviderCredentialsWithQuotaPreflight(
|
||||
"elevenlabs"
|
||||
)) as ElevenLabsCredentials | null;
|
||||
if (credentials && isAllRateLimitedCredentials(credentials)) {
|
||||
return rateLimitedProviderResponse("elevenlabs", credentials);
|
||||
}
|
||||
const apiKey = credentials?.apiKey || credentials?.accessToken;
|
||||
if (!apiKey || credentials?.allExpired) {
|
||||
return new Response(
|
||||
JSON.stringify(buildErrorBody(401, "No credentials for provider: elevenlabs")),
|
||||
{
|
||||
status: 401,
|
||||
headers: { ...CORS_HEADERS, "Content-Type": "application/json" },
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const incomingUrl = new URL(request.url);
|
||||
const upstreamUrl = new URL(`${ELEVENLABS_API_BASE}${pathname}`);
|
||||
upstreamUrl.search = incomingUrl.search;
|
||||
const headers = new Headers();
|
||||
headers.set("xi-api-key", apiKey);
|
||||
const contentType = request.headers.get("content-type");
|
||||
if (contentType) headers.set("content-type", contentType);
|
||||
const accept = request.headers.get("accept");
|
||||
if (accept) headers.set("accept", accept);
|
||||
|
||||
try {
|
||||
const upstream = await fetch(upstreamUrl, { ...init, headers });
|
||||
if (upstream.ok) {
|
||||
await clearRecoveredProviderState(credentials as Record<string, unknown>);
|
||||
}
|
||||
return new Response(upstream.body, {
|
||||
status: upstream.status,
|
||||
statusText: upstream.statusText,
|
||||
headers: proxyResponseHeaders(upstream),
|
||||
});
|
||||
} catch (error) {
|
||||
return new Response(
|
||||
JSON.stringify(
|
||||
buildErrorBody(
|
||||
502,
|
||||
sanitizeErrorMessage(
|
||||
error instanceof Error ? error.message : "ElevenLabs request failed"
|
||||
)
|
||||
)
|
||||
),
|
||||
{
|
||||
status: 502,
|
||||
headers: { ...CORS_HEADERS, "Content-Type": "application/json" },
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -28,7 +28,11 @@ import { getAllAudioModels } from "@omniroute/open-sse/config/audioRegistry";
|
||||
import { getAllModerationModels } from "@omniroute/open-sse/config/moderationRegistry";
|
||||
import { getAllVideoModels } from "@omniroute/open-sse/config/videoRegistry";
|
||||
import { getAllMusicModels } from "@omniroute/open-sse/config/musicRegistry";
|
||||
import { REGISTRY } from "@omniroute/open-sse/config/providerRegistry";
|
||||
import {
|
||||
getRegistryModelThinkingEfforts,
|
||||
getRegistryThinkingEfforts,
|
||||
REGISTRY,
|
||||
} from "@omniroute/open-sse/config/providerRegistry";
|
||||
import { CODEX_NATIVE_UNPREFIXED_MODELS } from "@omniroute/open-sse/services/model";
|
||||
import { isModelSelectable } from "@omniroute/open-sse/services/modelLifecycle";
|
||||
import { resolveNestedComboTargets } from "@omniroute/open-sse/services/combo";
|
||||
@@ -265,10 +269,6 @@ async function buildUnifiedModelsResponseCore(
|
||||
// try would let a crash here propagate as an unhandled rejection instead
|
||||
// (catalogCache.ts's in-flight coalescing does not fully consume rejections).
|
||||
const hiddenModelsByProvider = getHiddenModelsByProvider();
|
||||
const isModelHiddenBulk = (providerId: string, modelId: string): boolean => {
|
||||
const hiddenSet = hiddenModelsByProvider.get(providerId);
|
||||
return hiddenSet ? hiddenSet.has(modelId) : false;
|
||||
};
|
||||
let settings: Record<string, any> = {};
|
||||
try {
|
||||
settings = await getSettings();
|
||||
@@ -377,6 +377,35 @@ async function buildUnifiedModelsResponseCore(
|
||||
const resolvePublicOwnerId = (providerId: string, canonicalProviderId: string): string =>
|
||||
providerIdToPrefix[providerId] || canonicalProviderId;
|
||||
|
||||
// #11300: the visibility toggle on a provider's dashboard page persists the
|
||||
// hidden-model row under whatever key the route's `[id]` param happened to be
|
||||
// (a node UUID, an alias like `cc`/`gh`/`cx`, or a canonical provider id) —
|
||||
// see `PATCH /api/provider-models`. The catalog loops below each key their own
|
||||
// lookup differently (raw connection provider, canonical id, or alias), so a
|
||||
// single-key lookup missed the override whenever the write key and the read key
|
||||
// diverged. Check every key a model could plausibly have been hidden under:
|
||||
// the raw key passed in, its resolved canonical provider id, that canonical id's
|
||||
// alias, and the compatible-provider-node prefix for either.
|
||||
const isModelHiddenBulk = (
|
||||
providerKey: string | null | undefined,
|
||||
modelId: string,
|
||||
canonicalProviderId?: string | null
|
||||
): boolean => {
|
||||
if (!providerKey || !modelId) return false;
|
||||
const canonical = canonicalProviderId || resolveCanonicalProviderId(providerKey);
|
||||
const alias =
|
||||
providerIdToAlias[canonical] || providerIdToAlias[providerKey] || undefined;
|
||||
const nodePrefix = providerIdToPrefix[providerKey] || providerIdToPrefix[canonical];
|
||||
const keysToCheck = [providerKey, canonical, alias, nodePrefix].filter(
|
||||
(k): k is string => Boolean(k)
|
||||
);
|
||||
for (const key of keysToCheck) {
|
||||
const hiddenSet = hiddenModelsByProvider.get(key);
|
||||
if (hiddenSet?.has(modelId)) return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
// Get combos
|
||||
let combos = [];
|
||||
await yieldCatalogBuildTurn();
|
||||
@@ -560,7 +589,9 @@ async function buildUnifiedModelsResponseCore(
|
||||
modelId,
|
||||
target,
|
||||
eligibleConnectionIds,
|
||||
connectionCatalog || {}
|
||||
connectionCatalog || {},
|
||||
getRegistryModelThinkingEfforts(providerId, modelId),
|
||||
getRegistryThinkingEfforts(providerId, modelId)
|
||||
);
|
||||
if (
|
||||
connectionEfforts === undefined &&
|
||||
@@ -644,7 +675,7 @@ async function buildUnifiedModelsResponseCore(
|
||||
providerId,
|
||||
modelId,
|
||||
canonical.capabilities.supportsThinking,
|
||||
registryModel?.supportedThinkingEfforts,
|
||||
getRegistryThinkingEfforts(providerId, modelId),
|
||||
true
|
||||
)
|
||||
: getThinkingCapabilityFields(
|
||||
@@ -955,7 +986,7 @@ async function buildUnifiedModelsResponseCore(
|
||||
if (!isModelSelectable(canonicalProviderId, model.id)) continue;
|
||||
if (!providerSupportsModel(canonicalProviderId, model.id)) continue;
|
||||
const aliasId = `${alias}/${model.id}`;
|
||||
if (isModelHiddenBulk(canonicalProviderId, model.id)) continue;
|
||||
if (isModelHiddenBulk(alias, model.id, canonicalProviderId)) continue;
|
||||
if (isExcludedByProviderConnections(canonicalProviderId, model.id)) continue;
|
||||
if (shouldHidePaid(canonicalProviderId, model.id, (model as { pricing?: unknown }).pricing))
|
||||
continue;
|
||||
@@ -1018,7 +1049,15 @@ async function buildUnifiedModelsResponseCore(
|
||||
|
||||
for (const modelId of CODEX_NATIVE_UNPREFIXED_MODELS) {
|
||||
if (!providerSupportsModel("codex", modelId)) continue;
|
||||
if (isModelHiddenBulk("codex", modelId)) continue;
|
||||
// #11300: a codex-native unprefixed model can also be hidden via the
|
||||
// `openai` provider page (codex runs on the openai-compatible connection)
|
||||
// or via the `cx` alias — check all three so a hide from any of them
|
||||
// suppresses the bare model id here.
|
||||
if (
|
||||
isModelHiddenBulk("codex", modelId) ||
|
||||
isModelHiddenBulk("openai", modelId)
|
||||
)
|
||||
continue;
|
||||
|
||||
const alias = providerIdToAlias.codex || "cx";
|
||||
const aliasId = `${alias}/${modelId}`;
|
||||
@@ -1079,7 +1118,7 @@ async function buildUnifiedModelsResponseCore(
|
||||
if (canonicalProviderId === "codex" && isCodexDiscoveryModelExcluded(sm)) {
|
||||
continue;
|
||||
}
|
||||
if (isModelHiddenBulk(providerId, sm.id)) continue;
|
||||
if (isModelHiddenBulk(providerId, sm.id, canonicalProviderId)) continue;
|
||||
if (isExcludedByProviderConnections(canonicalProviderId, sm.id)) continue;
|
||||
// #6457: some upstream discovery catalogs (e.g. HuggingFace's live
|
||||
// `/v1/models`) return image/diffusion models with no modality info,
|
||||
@@ -1498,7 +1537,7 @@ async function buildUnifiedModelsResponseCore(
|
||||
if (!isUnifiedChatSourceModelSelectable(canonicalProviderId, { ...model, id: modelId }))
|
||||
continue;
|
||||
if (model.isHidden === true) continue;
|
||||
if (isModelHiddenBulk(canonicalProviderId, modelId)) continue;
|
||||
if (isModelHiddenBulk(providerId, modelId, canonicalProviderId)) continue;
|
||||
if (isExcludedByProviderConnections(canonicalProviderId, modelId)) continue;
|
||||
// #6328: apply hidePaidModels to user-defined custom rows too.
|
||||
// Custom entries do not carry pricing, so shouldHidePaid() decides
|
||||
@@ -1682,7 +1721,7 @@ async function buildUnifiedModelsResponseCore(
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isModelHiddenBulk(canonicalProviderId, modelId)) continue;
|
||||
if (isModelHiddenBulk(providerKey, modelId, canonicalProviderId)) continue;
|
||||
if (isExcludedByProviderConnections(canonicalProviderId, modelId)) continue;
|
||||
// #6328: apply hidePaidModels to alias-backed rows too. Alias mappings
|
||||
// point at providerKey/modelId with no pricing, so shouldHidePaid()
|
||||
@@ -1756,7 +1795,7 @@ async function buildUnifiedModelsResponseCore(
|
||||
for (const model of fallbackModels) {
|
||||
const modelId = typeof model.id === "string" ? model.id : null;
|
||||
if (!modelId) continue;
|
||||
if (isModelHiddenBulk(canonicalProviderId, modelId)) continue;
|
||||
if (isModelHiddenBulk(providerId, modelId, canonicalProviderId)) continue;
|
||||
if (isExcludedByProviderConnections(canonicalProviderId, modelId)) continue;
|
||||
// #6328: apply hidePaidModels to managed-fallback rows too. Compatible
|
||||
// provider fallbacks lack pricing; shouldHidePaid() decides via the
|
||||
|
||||
@@ -36,6 +36,7 @@ export type ComboCatalogTarget = {
|
||||
|
||||
type ConnectionScopedReasoningModel = {
|
||||
id: string;
|
||||
supportsThinking?: boolean;
|
||||
supportedThinkingEfforts?: string[];
|
||||
};
|
||||
|
||||
@@ -106,7 +107,9 @@ export function getConnectionScopedEffortTiers(
|
||||
modelId: string,
|
||||
target: Pick<ComboCatalogTarget, "connectionId" | "allowedConnectionIds">,
|
||||
eligibleConnectionIds: readonly string[] | undefined,
|
||||
modelsByConnection: ConnectionScopedReasoningCatalog
|
||||
modelsByConnection: ConnectionScopedReasoningCatalog,
|
||||
explicitThinkingEfforts?: readonly string[],
|
||||
fallbackThinkingEfforts?: readonly string[]
|
||||
): string[] | undefined {
|
||||
const eligible = eligibleConnectionIds ? new Set(eligibleConnectionIds) : undefined;
|
||||
if (target.connectionId && eligible && !eligible.has(target.connectionId)) return [];
|
||||
@@ -139,7 +142,16 @@ export function getConnectionScopedEffortTiers(
|
||||
);
|
||||
if (matching.some((model) => model === undefined)) return [];
|
||||
|
||||
const efforts = matching.map((model) => model?.supportedThinkingEfforts || []);
|
||||
const efforts = matching.map((model) => {
|
||||
const resolved = model?.supportedThinkingEfforts?.length
|
||||
? model.supportedThinkingEfforts
|
||||
: model?.supportsThinking === true && fallbackThinkingEfforts
|
||||
? [...fallbackThinkingEfforts]
|
||||
: [];
|
||||
return explicitThinkingEfforts
|
||||
? explicitThinkingEfforts.filter((effort) => resolved.includes(effort))
|
||||
: resolved;
|
||||
});
|
||||
return intersectStringArrays(efforts);
|
||||
}
|
||||
|
||||
|
||||
@@ -27,9 +27,14 @@
|
||||
// refactors. (Confirmed convention: grep "from \"@omniroute/open-sse" src/app/api/v1/models/)
|
||||
import { getLearnedReasoningEffortForModel } from "@omniroute/open-sse/services/learnedReasoningEffortCaps.ts";
|
||||
import { isSkippedEffortProvider } from "@omniroute/open-sse/utils/syncedEffortVariants.ts";
|
||||
import {
|
||||
getRegistryModelThinkingEfforts,
|
||||
getRegistryThinkingEfforts,
|
||||
} from "@omniroute/open-sse/config/providerRegistry.ts";
|
||||
|
||||
interface SyncedCapabilityFlags {
|
||||
id?: string;
|
||||
supportsThinking?: boolean;
|
||||
supportsVision?: boolean;
|
||||
supportedThinkingEfforts?: string[];
|
||||
}
|
||||
@@ -37,10 +42,23 @@ interface SyncedCapabilityFlags {
|
||||
function effectiveEffortTiers(sm: SyncedCapabilityFlags, ownedBy: string): string[] | undefined {
|
||||
if (isSkippedEffortProvider(ownedBy)) return undefined;
|
||||
const learned = sm.id ? getLearnedReasoningEffortForModel(sm.id) : null;
|
||||
const synced =
|
||||
Array.isArray(sm.supportedThinkingEfforts) && sm.supportedThinkingEfforts.length > 0
|
||||
? sm.supportedThinkingEfforts
|
||||
: null;
|
||||
const explicit = sm.id ? getRegistryModelThinkingEfforts(ownedBy, sm.id) : undefined;
|
||||
if (explicit) {
|
||||
const observed = learned ? [...learned] : synced;
|
||||
const narrowed = observed
|
||||
? explicit.filter((effort) => observed.includes(effort))
|
||||
: [...explicit];
|
||||
return narrowed.length > 0 ? narrowed : undefined;
|
||||
}
|
||||
if (learned) return [...learned];
|
||||
return Array.isArray(sm.supportedThinkingEfforts) && sm.supportedThinkingEfforts.length > 0
|
||||
? sm.supportedThinkingEfforts
|
||||
: undefined;
|
||||
if (synced) return synced;
|
||||
if (!sm.supportsThinking || !sm.id) return undefined;
|
||||
const registryEfforts = getRegistryThinkingEfforts(ownedBy, sm.id);
|
||||
return registryEfforts && registryEfforts.length > 0 ? [...registryEfforts] : undefined;
|
||||
}
|
||||
|
||||
/** Build the `capabilities` object for a fresh synced-model catalog entry, or `undefined` when neither flag applies. */
|
||||
|
||||
16
src/app/api/v1/speech-to-text/route.ts
Normal file
16
src/app/api/v1/speech-to-text/route.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import {
|
||||
elevenLabsOptionsResponse,
|
||||
proxyElevenLabsRequest,
|
||||
} from "@/app/api/v1/_shared/elevenLabsProxy";
|
||||
|
||||
export async function OPTIONS() {
|
||||
return elevenLabsOptionsResponse();
|
||||
}
|
||||
|
||||
export async function POST(request: Request) {
|
||||
return proxyElevenLabsRequest(request, "/speech-to-text", {
|
||||
method: "POST",
|
||||
body: request.body,
|
||||
duplex: "half",
|
||||
});
|
||||
}
|
||||
29
src/app/api/v1/text-to-speech/[voiceId]/route.ts
Normal file
29
src/app/api/v1/text-to-speech/[voiceId]/route.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import {
|
||||
elevenLabsOptionsResponse,
|
||||
isSafeElevenLabsVoiceId,
|
||||
proxyElevenLabsRequest,
|
||||
} from "@/app/api/v1/_shared/elevenLabsProxy";
|
||||
import { buildErrorBody } from "@omniroute/open-sse/utils/error.ts";
|
||||
import { CORS_HEADERS } from "@/shared/utils/cors";
|
||||
|
||||
export async function OPTIONS() {
|
||||
return elevenLabsOptionsResponse();
|
||||
}
|
||||
|
||||
export async function POST(
|
||||
request: Request,
|
||||
{ params }: { params: Promise<{ voiceId: string }> }
|
||||
) {
|
||||
const { voiceId } = await params;
|
||||
if (!isSafeElevenLabsVoiceId(voiceId)) {
|
||||
return new Response(JSON.stringify(buildErrorBody(400, "Invalid ElevenLabs voice ID")), {
|
||||
status: 400,
|
||||
headers: { ...CORS_HEADERS, "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
return proxyElevenLabsRequest(request, `/text-to-speech/${voiceId}`, {
|
||||
method: "POST",
|
||||
body: request.body,
|
||||
duplex: "half",
|
||||
});
|
||||
}
|
||||
12
src/app/api/v1/voices/route.ts
Normal file
12
src/app/api/v1/voices/route.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import {
|
||||
elevenLabsOptionsResponse,
|
||||
proxyElevenLabsRequest,
|
||||
} from "@/app/api/v1/_shared/elevenLabsProxy";
|
||||
|
||||
export async function OPTIONS() {
|
||||
return elevenLabsOptionsResponse();
|
||||
}
|
||||
|
||||
export async function GET(request: Request) {
|
||||
return proxyElevenLabsRequest(request, "/voices");
|
||||
}
|
||||
@@ -974,7 +974,14 @@
|
||||
"batchFileUsedByCount": "مستخدم بواسطة {count, plural, one {دفعة واحدة} other {# دفعات}}",
|
||||
"batchFilePreview": "معاينة",
|
||||
"batchFilePreviewTruncated": "عرض {shown} سطرًا أوليًا ({total} سطرًا إجماليًا)",
|
||||
"batchFileDownloadFull": "تحميل الملف الكامل"
|
||||
"batchFileDownloadFull": "تحميل الملف الكامل",
|
||||
"batchHeaderSubtitle": "Run many requests as one job",
|
||||
"batchStep1": "1 · Upload JSONL",
|
||||
"batchStep1Desc": "Add requests",
|
||||
"batchStep2": "2 · Create batch",
|
||||
"batchStep2Desc": "Run job",
|
||||
"batchStep3": "3 · Get results",
|
||||
"batchStep3Desc": "Download output"
|
||||
},
|
||||
"disabled": "معطل",
|
||||
"featureFlagOmnirouteEmergencyFallbackDescription": "توجيه الطلبات التي استنفدت الميزانية إلى موفر/نموذج الاحتياط المجاني للطوارئ.",
|
||||
@@ -1293,7 +1300,8 @@
|
||||
"open": "فتح",
|
||||
"close": "إغلاق"
|
||||
},
|
||||
"noResults": "لا توجد نتائج"
|
||||
"noResults": "لا توجد نتائج",
|
||||
"trafficInspectorPurpose": "See exactly what your application sends to and receives from AI providers. Works with any OpenAI-compatible client."
|
||||
},
|
||||
"webhooks": {
|
||||
"title": "خطافات الويب",
|
||||
@@ -1856,7 +1864,21 @@
|
||||
"directDownloadHint": "أو قم بتنزيل تنسيق المثبت المعني مباشرة:",
|
||||
"releaseNotes": "ملاحظات الإصدار",
|
||||
"readMore": "اقرأ المزيد",
|
||||
"noAuthLabel": "لا يوجد مصادقة"
|
||||
"noAuthLabel": "لا يوجد مصادقة",
|
||||
"recentRequests": "Recent Requests",
|
||||
"recentRequestsEmpty": "No requests yet.",
|
||||
"recentRequestsModel": "Model",
|
||||
"recentRequestsTokens": "In / Out",
|
||||
"recentRequestsWhen": "When",
|
||||
"readinessEyebrow": "Get ready to route",
|
||||
"readinessTitle": "Send your first request",
|
||||
"readinessSubtitle": "Four small steps. OmniRoute checks readiness as you go.",
|
||||
"readinessStep1": "Connect a provider",
|
||||
"readinessStep2": "Configure endpoint authentication",
|
||||
"readinessStep3": "Copy your endpoint",
|
||||
"readinessStep4": "Send a test request",
|
||||
"readinessContinue": "Continue setup",
|
||||
"readinessDismiss": "Dismiss for now"
|
||||
},
|
||||
"analytics": {
|
||||
"title": "التحليلات",
|
||||
@@ -2901,7 +2923,8 @@
|
||||
"omp": "عميل برمجة الطرفية Oh My Pi",
|
||||
"letta": "عميل Letta CLI بذاكرة مستمرة واستخدام للأدوات",
|
||||
"warp": "طرفية Warp AI مع دعم لمزودي الخدمة المخصصين",
|
||||
"agent-deck": "منسق الوكلاء المتعددين Agent Deck"
|
||||
"agent-deck": "منسق الوكلاء المتعددين Agent Deck",
|
||||
"prime-agent": "Prime Agent — self-improving RLM coding harness with OpenAI-compatible provider support"
|
||||
},
|
||||
"guides": {
|
||||
"cursor": {
|
||||
@@ -4133,7 +4156,10 @@
|
||||
"notionIntegrationHelp": "إنشاء تكامل داخلي في",
|
||||
"notionIntegrationToken": "رمز تكامل Notion الداخلي",
|
||||
"notionNotConnected": "غير متصل",
|
||||
"notionTokenConfigured": "تم تكوين الرمز. أدوات Notion متاحة عبر MCP."
|
||||
"notionTokenConfigured": "تم تكوين الرمز. أدوات Notion متاحة عبر MCP.",
|
||||
"subtitle": "Use the OpenAI-compatible endpoint with most SDKs and tools.",
|
||||
"testEndpoint": "Test endpoint →",
|
||||
"advancedProtocols": "Advanced protocols"
|
||||
},
|
||||
"endpoints": {
|
||||
"tabProxy": "بروكسي نقطة النهاية",
|
||||
@@ -4716,7 +4742,14 @@
|
||||
"issueCount": "قضايا {count}",
|
||||
"score": "النتيجة",
|
||||
"lastRequest": "الطلب الأخير",
|
||||
"lastError": "الخطأ الأخير"
|
||||
"lastError": "الخطأ الأخير",
|
||||
"healthVerdictReady": "OmniRoute is ready",
|
||||
"healthVerdictActionRequired": "Action required to restore full operation",
|
||||
"healthVerdictCoolingDown": "Cooling down after recent changes",
|
||||
"healthSubtitle": "System health check",
|
||||
"advancedDiagnosticsTitle": "Advanced diagnostics",
|
||||
"hide": "Hide",
|
||||
"show": "Show"
|
||||
},
|
||||
"telemetry": {
|
||||
"title": "القياس عن بعد للنظام",
|
||||
@@ -6383,7 +6416,21 @@
|
||||
"savedModelEndpointSettings": "إعدادات نقطة نهاية النموذج المحفوظ",
|
||||
"searchByModelAria": "البحث حسب الطراز",
|
||||
"selectSupportedEndpoint": "اختر نقطة نهاية مدعومة واحدة على الأقل",
|
||||
"antigravityClientProfileHarness": "Harness / CLI"
|
||||
"antigravityClientProfileHarness": "Harness / CLI",
|
||||
"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."
|
||||
},
|
||||
"settings": {
|
||||
"title": "الإعدادات",
|
||||
@@ -8230,7 +8277,19 @@
|
||||
"cliproxyapiHealth": "الصحة",
|
||||
"cliproxyapiPort": "منفذ",
|
||||
"qdrantHost": "مضيف",
|
||||
"qdrantCollection": "مجموعة"
|
||||
"qdrantCollection": "مجموعة",
|
||||
"presetAll": "All",
|
||||
"presetAllDesc": "Show everything",
|
||||
"presetEssentials": "Essentials",
|
||||
"presetEssentialsDesc": "Beginner path - Advanced tools stay searchable",
|
||||
"presetMinimal": "Minimal",
|
||||
"presetMinimalDesc": "Core pages only",
|
||||
"presetDeveloper": "Developer",
|
||||
"presetDeveloperDesc": "Dev & proxy tools",
|
||||
"presetAdmin": "Admin",
|
||||
"presetAdminDesc": "Monitoring & audit",
|
||||
"settingsSidebarTitle": "Sidebar Customization",
|
||||
"settingsSidebarDesc": "Choose which sidebar items to show. Essentials keeps Advanced tools searchable."
|
||||
},
|
||||
"contextRtk": {
|
||||
"title": "محرك آر تي كيه",
|
||||
@@ -12017,7 +12076,8 @@
|
||||
"title": "وكلاء ACP",
|
||||
"phrase": "واجهات CLI التي يقوم OmniRoute بإنشائها كخلفية تنفيذ (تدفق عكسي)",
|
||||
"flow": "العميل → OmniRoute → إنشاء CLI (stdio/ACP) → الاستجابة",
|
||||
"seeOther": "عرض →"
|
||||
"seeOther": "عرض →",
|
||||
"warning": "Most users can ignore this — use only when an integration requires it."
|
||||
}
|
||||
},
|
||||
"comparison": {
|
||||
@@ -12841,6 +12901,10 @@
|
||||
},
|
||||
"SKILLS_SANDBOX_NETWORK_ENABLED": {
|
||||
"description": "تمكين الوصول إلى الشبكة في بيئة اختبار المهارات المعزولة."
|
||||
},
|
||||
"DISABLE_CONTEXT_WINDOW_CHECKS": {
|
||||
"label": "Disable Context Window Checks",
|
||||
"description": "Skip OmniRoute's local context-window and max-input-token check for direct single-model requests. Upstream providers still enforce their actual limits. Prompt compression and output-token caps remain active."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -13413,6 +13477,13 @@
|
||||
"modelLockouts": "حظر النماذج",
|
||||
"count": "عدد الاتصالات"
|
||||
}
|
||||
},
|
||||
"reassuranceTitle": "Your connections recover automatically",
|
||||
"reassuranceDetail": "Usually no action is needed. OmniRoute temporarily rests a connection after failures, then safely tries it again.",
|
||||
"plainStates": {
|
||||
"healthy": "Requests can be sent",
|
||||
"coolingDown": "Trying again soon",
|
||||
"lockedOut": "Needs your attention"
|
||||
}
|
||||
},
|
||||
"featureFlagCapabilityFilterEnabledDescription": "رفض الطلبات قبل الإرسال عندما يفتقر النموذج المستهدف إلى القدرات المطلوبة (الرؤية، الأدوات، المخرجات المنظمة، نافذة السياق). يحمي الطلبات المباشرة من مزود واحد التي تتجاوز فلتر توافق الطبقة المجمعة.",
|
||||
@@ -13850,5 +13921,13 @@
|
||||
"toolsMismatch": "المزود لا يدعم استدعاء الأداة",
|
||||
"structuredOutputMismatch": "المزود لا يدعم الإخراج المنظم",
|
||||
"contextWindowMismatch": "تجاوز الطلب نافذة سياق المزود"
|
||||
},
|
||||
"featureFlagDisableContextWindowChecksDescription": "Skip OmniRoute's local context-window and max-input-token check for direct single-model requests. Upstream providers still enforce their actual limits. Prompt compression and output-token caps remain active.",
|
||||
"cheaperInferenceSponsorBanner": {
|
||||
"title": "Cheaper Inference is an OmniRoute Open Source Friend",
|
||||
"description": "A cost-ranked gateway reselling dozens of frontier models behind one OpenAI-compatible endpoint — routing each request to the cheapest eligible provider, never above list price.",
|
||||
"cta": "Get an API Key",
|
||||
"partnerLinkNote": "Partner link",
|
||||
"dismissAriaLabel": "Dismiss"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -974,7 +974,14 @@
|
||||
"batchFileUsedByCount": "{count, plural, one {# partiya} other {# partiyalar}}",
|
||||
"batchFilePreview": "Önizləmə",
|
||||
"batchFilePreviewTruncated": "İlk {shown} sətir göstərilir ({total} cəmi sətir)",
|
||||
"batchFileDownloadFull": "Tam Faylı Yüklə"
|
||||
"batchFileDownloadFull": "Tam Faylı Yüklə",
|
||||
"batchHeaderSubtitle": "Run many requests as one job",
|
||||
"batchStep1": "1 · Upload JSONL",
|
||||
"batchStep1Desc": "Add requests",
|
||||
"batchStep2": "2 · Create batch",
|
||||
"batchStep2Desc": "Run job",
|
||||
"batchStep3": "3 · Get results",
|
||||
"batchStep3Desc": "Download output"
|
||||
},
|
||||
"disabled": "Deaktiv",
|
||||
"featureFlagOmnirouteEmergencyFallbackDescription": "Büdcəsi tükənmiş sorğuları təcili pulsuz ehtiyat təminatçıya/modelə yönləndirin.",
|
||||
@@ -1293,7 +1300,8 @@
|
||||
"open": "açıq",
|
||||
"close": "bağla"
|
||||
},
|
||||
"noResults": "Heç bir nəticə yoxdur"
|
||||
"noResults": "Heç bir nəticə yoxdur",
|
||||
"trafficInspectorPurpose": "See exactly what your application sends to and receives from AI providers. Works with any OpenAI-compatible client."
|
||||
},
|
||||
"webhooks": {
|
||||
"title": "Webhooks",
|
||||
@@ -1856,7 +1864,21 @@
|
||||
"directDownloadHint": "Yaxud müvafiq quraşdırıcı formatını birbaşa yükləyin:",
|
||||
"releaseNotes": "Buraxılış Qeydləri",
|
||||
"readMore": "Daha Çox Oxu",
|
||||
"noAuthLabel": "No Auth"
|
||||
"noAuthLabel": "No Auth",
|
||||
"recentRequests": "Recent Requests",
|
||||
"recentRequestsEmpty": "No requests yet.",
|
||||
"recentRequestsModel": "Model",
|
||||
"recentRequestsTokens": "In / Out",
|
||||
"recentRequestsWhen": "When",
|
||||
"readinessEyebrow": "Get ready to route",
|
||||
"readinessTitle": "Send your first request",
|
||||
"readinessSubtitle": "Four small steps. OmniRoute checks readiness as you go.",
|
||||
"readinessStep1": "Connect a provider",
|
||||
"readinessStep2": "Configure endpoint authentication",
|
||||
"readinessStep3": "Copy your endpoint",
|
||||
"readinessStep4": "Send a test request",
|
||||
"readinessContinue": "Continue setup",
|
||||
"readinessDismiss": "Dismiss for now"
|
||||
},
|
||||
"analytics": {
|
||||
"title": "Analytics",
|
||||
@@ -2901,7 +2923,8 @@
|
||||
"omp": "Oh My Pi terminal kodlaşdırma agenti",
|
||||
"letta": "Davamlı yaddaşa və alət istifadəsinə malik Letta CLI agenti",
|
||||
"warp": "Fərdi provayder dəstəyinə malik Warp AI terminalı",
|
||||
"agent-deck": "Agent Deck çoxagentli orkestratoru"
|
||||
"agent-deck": "Agent Deck çoxagentli orkestratoru",
|
||||
"prime-agent": "Prime Agent — self-improving RLM coding harness with OpenAI-compatible provider support"
|
||||
},
|
||||
"guides": {
|
||||
"cursor": {
|
||||
@@ -4133,7 +4156,10 @@
|
||||
"notionIntegrationHelp": "İç İnteqrasiya Yaradın at",
|
||||
"notionIntegrationToken": "Notion Daxili İnteqrasiya Tokeni",
|
||||
"notionNotConnected": "Bağlı deyil",
|
||||
"notionTokenConfigured": "Token konfiqurasiya edilib. Notion alətləri MCP vasitəsilə mövcuddur."
|
||||
"notionTokenConfigured": "Token konfiqurasiya edilib. Notion alətləri MCP vasitəsilə mövcuddur.",
|
||||
"subtitle": "Use the OpenAI-compatible endpoint with most SDKs and tools.",
|
||||
"testEndpoint": "Test endpoint →",
|
||||
"advancedProtocols": "Advanced protocols"
|
||||
},
|
||||
"endpoints": {
|
||||
"tabProxy": "Endpoint Proxy",
|
||||
@@ -4716,7 +4742,14 @@
|
||||
"issueCount": "{count} problem",
|
||||
"score": "Xal",
|
||||
"lastRequest": "Son sorğu",
|
||||
"lastError": "Son xəta"
|
||||
"lastError": "Son xəta",
|
||||
"healthVerdictReady": "OmniRoute is ready",
|
||||
"healthVerdictActionRequired": "Action required to restore full operation",
|
||||
"healthVerdictCoolingDown": "Cooling down after recent changes",
|
||||
"healthSubtitle": "System health check",
|
||||
"advancedDiagnosticsTitle": "Advanced diagnostics",
|
||||
"hide": "Hide",
|
||||
"show": "Show"
|
||||
},
|
||||
"telemetry": {
|
||||
"title": "System Telemetry",
|
||||
@@ -6383,7 +6416,21 @@
|
||||
"savedModelEndpointSettings": "Saxlanmış model son nöqtəsi parametrləri",
|
||||
"searchByModelAria": "Model üzrə axtarış edin",
|
||||
"selectSupportedEndpoint": "Ən azı bir dəstəklənən son nöqtəni seçin",
|
||||
"antigravityClientProfileHarness": "Harness / CLI"
|
||||
"antigravityClientProfileHarness": "Harness / CLI",
|
||||
"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."
|
||||
},
|
||||
"settings": {
|
||||
"title": "Settings",
|
||||
@@ -8230,7 +8277,19 @@
|
||||
"cliproxyapiHealth": "Sağlamlıq",
|
||||
"cliproxyapiPort": "Port",
|
||||
"qdrantHost": "Ev sahibi",
|
||||
"qdrantCollection": "Kolleksiya"
|
||||
"qdrantCollection": "Kolleksiya",
|
||||
"presetAll": "All",
|
||||
"presetAllDesc": "Show everything",
|
||||
"presetEssentials": "Essentials",
|
||||
"presetEssentialsDesc": "Beginner path - Advanced tools stay searchable",
|
||||
"presetMinimal": "Minimal",
|
||||
"presetMinimalDesc": "Core pages only",
|
||||
"presetDeveloper": "Developer",
|
||||
"presetDeveloperDesc": "Dev & proxy tools",
|
||||
"presetAdmin": "Admin",
|
||||
"presetAdminDesc": "Monitoring & audit",
|
||||
"settingsSidebarTitle": "Sidebar Customization",
|
||||
"settingsSidebarDesc": "Choose which sidebar items to show. Essentials keeps Advanced tools searchable."
|
||||
},
|
||||
"contextRtk": {
|
||||
"title": "RTK Engine",
|
||||
@@ -12017,7 +12076,8 @@
|
||||
"title": "ACP Agentləri",
|
||||
"phrase": "OmniRoute-un icra backend-i kimi başlatdığı CLI-lar (əks axın)",
|
||||
"flow": "Klient → OmniRoute → CLI başlat (stdio/ACP) → cavab",
|
||||
"seeOther": "Bax →"
|
||||
"seeOther": "Bax →",
|
||||
"warning": "Most users can ignore this — use only when an integration requires it."
|
||||
}
|
||||
},
|
||||
"comparison": {
|
||||
@@ -12841,6 +12901,10 @@
|
||||
},
|
||||
"SKILLS_SANDBOX_NETWORK_ENABLED": {
|
||||
"description": "Bacarıqlar sandbox-unda şəbəkəyə girişi aktivləşdirin."
|
||||
},
|
||||
"DISABLE_CONTEXT_WINDOW_CHECKS": {
|
||||
"label": "Disable Context Window Checks",
|
||||
"description": "Skip OmniRoute's local context-window and max-input-token check for direct single-model requests. Upstream providers still enforce their actual limits. Prompt compression and output-token caps remain active."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -13413,6 +13477,13 @@
|
||||
"modelLockouts": "Model Blokları",
|
||||
"count": "Bağlantı Sayı"
|
||||
}
|
||||
},
|
||||
"reassuranceTitle": "Your connections recover automatically",
|
||||
"reassuranceDetail": "Usually no action is needed. OmniRoute temporarily rests a connection after failures, then safely tries it again.",
|
||||
"plainStates": {
|
||||
"healthy": "Requests can be sent",
|
||||
"coolingDown": "Trying again soon",
|
||||
"lockedOut": "Needs your attention"
|
||||
}
|
||||
},
|
||||
"featureFlagCapabilityFilterEnabledDescription": "Tələb olunan imkanlar (görmə, alətlər, strukturlaşdırılmış çıxış, kontekst pəncərəsi) olmayan hədəf modelində göndərilmədən əvvəl tələbləri rədd edin. Kombinasiya qatının uyğunluq filtrini keçən birbaşa tək təminatçı tələblərini qoruyur.",
|
||||
@@ -13850,5 +13921,13 @@
|
||||
"toolsMismatch": "Təchizatçı alət çağırışını dəstəkləmir",
|
||||
"structuredOutputMismatch": "Təchizatçı strukturlaşdırılmış çıxışı dəstəkləmir",
|
||||
"contextWindowMismatch": "Sorğu təminatçının kontekst pəncərəsini aşır"
|
||||
},
|
||||
"featureFlagDisableContextWindowChecksDescription": "Skip OmniRoute's local context-window and max-input-token check for direct single-model requests. Upstream providers still enforce their actual limits. Prompt compression and output-token caps remain active.",
|
||||
"cheaperInferenceSponsorBanner": {
|
||||
"title": "Cheaper Inference is an OmniRoute Open Source Friend",
|
||||
"description": "A cost-ranked gateway reselling dozens of frontier models behind one OpenAI-compatible endpoint — routing each request to the cheapest eligible provider, never above list price.",
|
||||
"cta": "Get an API Key",
|
||||
"partnerLinkNote": "Partner link",
|
||||
"dismissAriaLabel": "Dismiss"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -974,7 +974,14 @@
|
||||
"batchFileUsedByCount": "Използвано от {count, plural, one {# партида} other {# партиди}}",
|
||||
"batchFilePreview": "Преглед",
|
||||
"batchFilePreviewTruncated": "Показване на първите {shown} реда ({total} общо реда)",
|
||||
"batchFileDownloadFull": "Изтеглете целия файл"
|
||||
"batchFileDownloadFull": "Изтеглете целия файл",
|
||||
"batchHeaderSubtitle": "Run many requests as one job",
|
||||
"batchStep1": "1 · Upload JSONL",
|
||||
"batchStep1Desc": "Add requests",
|
||||
"batchStep2": "2 · Create batch",
|
||||
"batchStep2Desc": "Run job",
|
||||
"batchStep3": "3 · Get results",
|
||||
"batchStep3Desc": "Download output"
|
||||
},
|
||||
"disabled": "Деактивирано",
|
||||
"featureFlagOmnirouteEmergencyFallbackDescription": "Маршрутизиране на заявки с изчерпан бюджет към аварийния безплатен резервен доставчик/модел.",
|
||||
@@ -1293,7 +1300,8 @@
|
||||
"open": "отвори",
|
||||
"close": "затвори"
|
||||
},
|
||||
"noResults": "Няма резултати"
|
||||
"noResults": "Няма резултати",
|
||||
"trafficInspectorPurpose": "See exactly what your application sends to and receives from AI providers. Works with any OpenAI-compatible client."
|
||||
},
|
||||
"webhooks": {
|
||||
"title": "Уеб кукички",
|
||||
@@ -1856,7 +1864,21 @@
|
||||
"directDownloadHint": "Или изтеглете съответния инсталаторен формат директно:",
|
||||
"releaseNotes": "Бележки за изданието",
|
||||
"readMore": "Прочетете повече",
|
||||
"noAuthLabel": "Без удостоверяване"
|
||||
"noAuthLabel": "Без удостоверяване",
|
||||
"recentRequests": "Recent Requests",
|
||||
"recentRequestsEmpty": "No requests yet.",
|
||||
"recentRequestsModel": "Model",
|
||||
"recentRequestsTokens": "In / Out",
|
||||
"recentRequestsWhen": "When",
|
||||
"readinessEyebrow": "Get ready to route",
|
||||
"readinessTitle": "Send your first request",
|
||||
"readinessSubtitle": "Four small steps. OmniRoute checks readiness as you go.",
|
||||
"readinessStep1": "Connect a provider",
|
||||
"readinessStep2": "Configure endpoint authentication",
|
||||
"readinessStep3": "Copy your endpoint",
|
||||
"readinessStep4": "Send a test request",
|
||||
"readinessContinue": "Continue setup",
|
||||
"readinessDismiss": "Dismiss for now"
|
||||
},
|
||||
"analytics": {
|
||||
"title": "Анализ",
|
||||
@@ -2901,7 +2923,8 @@
|
||||
"omp": "Oh My Pi терминален агент за програмиране",
|
||||
"letta": "Letta CLI агент с постоянна памет и използване на инструменти",
|
||||
"warp": "Warp AI терминал с поддръжка на персонализиран доставчик",
|
||||
"agent-deck": "Agent Deck мултиагентен оркестратор"
|
||||
"agent-deck": "Agent Deck мултиагентен оркестратор",
|
||||
"prime-agent": "Prime Agent — self-improving RLM coding harness with OpenAI-compatible provider support"
|
||||
},
|
||||
"guides": {
|
||||
"cursor": {
|
||||
@@ -4133,7 +4156,10 @@
|
||||
"notionIntegrationHelp": "Създайте вътрешна интеграция на",
|
||||
"notionIntegrationToken": "Токен за вътрешна интеграция на Notion",
|
||||
"notionNotConnected": "Не е свързано",
|
||||
"notionTokenConfigured": "Токенът е конфигуриран. Инструментите на Notion са налични чрез MCP."
|
||||
"notionTokenConfigured": "Токенът е конфигуриран. Инструментите на Notion са налични чрез MCP.",
|
||||
"subtitle": "Use the OpenAI-compatible endpoint with most SDKs and tools.",
|
||||
"testEndpoint": "Test endpoint →",
|
||||
"advancedProtocols": "Advanced protocols"
|
||||
},
|
||||
"endpoints": {
|
||||
"tabProxy": "Endpoint Proxy",
|
||||
@@ -4716,7 +4742,14 @@
|
||||
"issueCount": "{count} проблема",
|
||||
"score": "Оценка",
|
||||
"lastRequest": "Последна заявка",
|
||||
"lastError": "Последна грешка"
|
||||
"lastError": "Последна грешка",
|
||||
"healthVerdictReady": "OmniRoute is ready",
|
||||
"healthVerdictActionRequired": "Action required to restore full operation",
|
||||
"healthVerdictCoolingDown": "Cooling down after recent changes",
|
||||
"healthSubtitle": "System health check",
|
||||
"advancedDiagnosticsTitle": "Advanced diagnostics",
|
||||
"hide": "Hide",
|
||||
"show": "Show"
|
||||
},
|
||||
"telemetry": {
|
||||
"title": "Системна телеметрия",
|
||||
@@ -6383,7 +6416,21 @@
|
||||
"savedModelEndpointSettings": "Настройки на крайна точка на запазен модел",
|
||||
"searchByModelAria": "Търсене по модел",
|
||||
"selectSupportedEndpoint": "Изберете поне една поддържана крайна точка",
|
||||
"antigravityClientProfileHarness": "Harness / CLI"
|
||||
"antigravityClientProfileHarness": "Harness / CLI",
|
||||
"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."
|
||||
},
|
||||
"settings": {
|
||||
"title": "Настройки",
|
||||
@@ -8230,7 +8277,19 @@
|
||||
"cliproxyapiHealth": "Здраве",
|
||||
"cliproxyapiPort": "Порт",
|
||||
"qdrantHost": "Хост",
|
||||
"qdrantCollection": "Колекция"
|
||||
"qdrantCollection": "Колекция",
|
||||
"presetAll": "All",
|
||||
"presetAllDesc": "Show everything",
|
||||
"presetEssentials": "Essentials",
|
||||
"presetEssentialsDesc": "Beginner path - Advanced tools stay searchable",
|
||||
"presetMinimal": "Minimal",
|
||||
"presetMinimalDesc": "Core pages only",
|
||||
"presetDeveloper": "Developer",
|
||||
"presetDeveloperDesc": "Dev & proxy tools",
|
||||
"presetAdmin": "Admin",
|
||||
"presetAdminDesc": "Monitoring & audit",
|
||||
"settingsSidebarTitle": "Sidebar Customization",
|
||||
"settingsSidebarDesc": "Choose which sidebar items to show. Essentials keeps Advanced tools searchable."
|
||||
},
|
||||
"contextRtk": {
|
||||
"title": "RTK Engine",
|
||||
@@ -12017,7 +12076,8 @@
|
||||
"title": "ACP агенти",
|
||||
"phrase": "CLI, които OmniRoute стартира като бекенд за изпълнение (обратен поток)",
|
||||
"flow": "Клиент → OmniRoute → стартиране на CLI (stdio/ACP) → отговор",
|
||||
"seeOther": "Вижте →"
|
||||
"seeOther": "Вижте →",
|
||||
"warning": "Most users can ignore this — use only when an integration requires it."
|
||||
}
|
||||
},
|
||||
"comparison": {
|
||||
@@ -12841,6 +12901,10 @@
|
||||
},
|
||||
"SKILLS_SANDBOX_NETWORK_ENABLED": {
|
||||
"description": "Активиране на мрежов достъп в пясъчника за умения."
|
||||
},
|
||||
"DISABLE_CONTEXT_WINDOW_CHECKS": {
|
||||
"label": "Disable Context Window Checks",
|
||||
"description": "Skip OmniRoute's local context-window and max-input-token check for direct single-model requests. Upstream providers still enforce their actual limits. Prompt compression and output-token caps remain active."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -13413,6 +13477,13 @@
|
||||
"modelLockouts": "Заключвания на Модел",
|
||||
"count": "Брой Връзки"
|
||||
}
|
||||
},
|
||||
"reassuranceTitle": "Your connections recover automatically",
|
||||
"reassuranceDetail": "Usually no action is needed. OmniRoute temporarily rests a connection after failures, then safely tries it again.",
|
||||
"plainStates": {
|
||||
"healthy": "Requests can be sent",
|
||||
"coolingDown": "Trying again soon",
|
||||
"lockedOut": "Needs your attention"
|
||||
}
|
||||
},
|
||||
"featureFlagCapabilityFilterEnabledDescription": "Отхвърлете заявките преди изпращане, когато целевият модел няма необходимите възможности (визия, инструменти, структурирани изходи, контекстен прозорец). Защитава директните заявки от един доставчик, които заобикалят филтъра за съвместимост на комбинирания слой.",
|
||||
@@ -13850,5 +13921,13 @@
|
||||
"toolsMismatch": "Доставчикът не поддържа извикване на инструменти",
|
||||
"structuredOutputMismatch": "Доставчикът не поддържа структурирано изходно съдържание",
|
||||
"contextWindowMismatch": "Заявката надвишава контекстния прозорец на доставчика"
|
||||
},
|
||||
"featureFlagDisableContextWindowChecksDescription": "Skip OmniRoute's local context-window and max-input-token check for direct single-model requests. Upstream providers still enforce their actual limits. Prompt compression and output-token caps remain active.",
|
||||
"cheaperInferenceSponsorBanner": {
|
||||
"title": "Cheaper Inference is an OmniRoute Open Source Friend",
|
||||
"description": "A cost-ranked gateway reselling dozens of frontier models behind one OpenAI-compatible endpoint — routing each request to the cheapest eligible provider, never above list price.",
|
||||
"cta": "Get an API Key",
|
||||
"partnerLinkNote": "Partner link",
|
||||
"dismissAriaLabel": "Dismiss"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -974,7 +974,14 @@
|
||||
"batchFileUsedByCount": "ব্যবহৃত হয়েছে {count, plural, one {# ব্যাচ} other {# ব্যাচ}}",
|
||||
"batchFilePreview": "পূর্বদর্শন",
|
||||
"batchFilePreviewTruncated": "প্রথম {shown} লাইন দেখানো হচ্ছে ({total} মোট লাইন)",
|
||||
"batchFileDownloadFull": "পূর্ণ ফাইল ডাউনলোড করুন"
|
||||
"batchFileDownloadFull": "পূর্ণ ফাইল ডাউনলোড করুন",
|
||||
"batchHeaderSubtitle": "Run many requests as one job",
|
||||
"batchStep1": "1 · Upload JSONL",
|
||||
"batchStep1Desc": "Add requests",
|
||||
"batchStep2": "2 · Create batch",
|
||||
"batchStep2Desc": "Run job",
|
||||
"batchStep3": "3 · Get results",
|
||||
"batchStep3Desc": "Download output"
|
||||
},
|
||||
"disabled": "নিষ্ক্রিয়",
|
||||
"featureFlagOmnirouteEmergencyFallbackDescription": "বাজেট শেষ হয়ে যাওয়া অনুরোধগুলো জরুরি ফ্রি ফলব্যাক প্রোভাইডার/মডেলে রুট করুন।",
|
||||
@@ -1293,7 +1300,8 @@
|
||||
"open": "খুলুন",
|
||||
"close": "বন্ধ করুন"
|
||||
},
|
||||
"noResults": "কোন ফলাফল নেই"
|
||||
"noResults": "কোন ফলাফল নেই",
|
||||
"trafficInspectorPurpose": "See exactly what your application sends to and receives from AI providers. Works with any OpenAI-compatible client."
|
||||
},
|
||||
"webhooks": {
|
||||
"title": "ওয়েবহুক",
|
||||
@@ -1856,7 +1864,21 @@
|
||||
"directDownloadHint": "অথবা সংশ্লিষ্ট ইনস্টলার ফরম্যাটটি সরাসরি ডাউনলোড করুন:",
|
||||
"releaseNotes": "রিলিজ নোটস",
|
||||
"readMore": "আরও পড়ুন",
|
||||
"noAuthLabel": "কোন প্রমাণীকরণ নেই"
|
||||
"noAuthLabel": "কোন প্রমাণীকরণ নেই",
|
||||
"recentRequests": "Recent Requests",
|
||||
"recentRequestsEmpty": "No requests yet.",
|
||||
"recentRequestsModel": "Model",
|
||||
"recentRequestsTokens": "In / Out",
|
||||
"recentRequestsWhen": "When",
|
||||
"readinessEyebrow": "Get ready to route",
|
||||
"readinessTitle": "Send your first request",
|
||||
"readinessSubtitle": "Four small steps. OmniRoute checks readiness as you go.",
|
||||
"readinessStep1": "Connect a provider",
|
||||
"readinessStep2": "Configure endpoint authentication",
|
||||
"readinessStep3": "Copy your endpoint",
|
||||
"readinessStep4": "Send a test request",
|
||||
"readinessContinue": "Continue setup",
|
||||
"readinessDismiss": "Dismiss for now"
|
||||
},
|
||||
"analytics": {
|
||||
"title": "Analytics",
|
||||
@@ -2901,7 +2923,8 @@
|
||||
"omp": "Oh My Pi টার্মিনাল কোডিং এজেন্ট",
|
||||
"letta": "পারসিস্টেন্ট মেমরি এবং টুল ব্যবহারের সুবিধা সহ Letta CLI এজেন্ট",
|
||||
"warp": "কাস্টম প্রোভাইডার সাপোর্ট সহ Warp AI টার্মিনাল",
|
||||
"agent-deck": "Agent Deck মাল্টি-এজেন্ট অর্কেস্ট্রেটর"
|
||||
"agent-deck": "Agent Deck মাল্টি-এজেন্ট অর্কেস্ট্রেটর",
|
||||
"prime-agent": "Prime Agent — self-improving RLM coding harness with OpenAI-compatible provider support"
|
||||
},
|
||||
"guides": {
|
||||
"cursor": {
|
||||
@@ -4133,7 +4156,10 @@
|
||||
"notionIntegrationHelp": "একটি অভ্যন্তরীণ ইন্টিগ্রেশন তৈরি করুন at",
|
||||
"notionIntegrationToken": "নোটশন অভ্যন্তরীণ ইন্টিগ্রেশন টোকেন",
|
||||
"notionNotConnected": "সংযুক্ত নয়",
|
||||
"notionTokenConfigured": "টোকেন কনফিগার করা হয়েছে। Notion টুলগুলি MCP এর মাধ্যমে উপলব্ধ।"
|
||||
"notionTokenConfigured": "টোকেন কনফিগার করা হয়েছে। Notion টুলগুলি MCP এর মাধ্যমে উপলব্ধ।",
|
||||
"subtitle": "Use the OpenAI-compatible endpoint with most SDKs and tools.",
|
||||
"testEndpoint": "Test endpoint →",
|
||||
"advancedProtocols": "Advanced protocols"
|
||||
},
|
||||
"endpoints": {
|
||||
"tabProxy": "Endpoint Proxy",
|
||||
@@ -4716,7 +4742,14 @@
|
||||
"issueCount": "{count} সমস্যা",
|
||||
"score": "স্কোর",
|
||||
"lastRequest": "সর্বশেষ অনুরোধ",
|
||||
"lastError": "সর্বশেষ ত্রুটি"
|
||||
"lastError": "সর্বশেষ ত্রুটি",
|
||||
"healthVerdictReady": "OmniRoute is ready",
|
||||
"healthVerdictActionRequired": "Action required to restore full operation",
|
||||
"healthVerdictCoolingDown": "Cooling down after recent changes",
|
||||
"healthSubtitle": "System health check",
|
||||
"advancedDiagnosticsTitle": "Advanced diagnostics",
|
||||
"hide": "Hide",
|
||||
"show": "Show"
|
||||
},
|
||||
"telemetry": {
|
||||
"title": "সিস্টেম টেলিমেট্রি",
|
||||
@@ -6383,7 +6416,21 @@
|
||||
"savedModelEndpointSettings": "সংরক্ষিত মডেল এন্ডপয়েন্ট সেটিংস",
|
||||
"searchByModelAria": "মডেল দ্বারা অনুসন্ধান করুন",
|
||||
"selectSupportedEndpoint": "কমপক্ষে একটি সমর্থিত এন্ডপয়েন্ট নির্বাচন করুন",
|
||||
"antigravityClientProfileHarness": "Harness / CLI"
|
||||
"antigravityClientProfileHarness": "Harness / CLI",
|
||||
"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."
|
||||
},
|
||||
"settings": {
|
||||
"title": "Settings",
|
||||
@@ -8230,7 +8277,19 @@
|
||||
"cliproxyapiHealth": "স্বাস্থ্য",
|
||||
"cliproxyapiPort": "পোর্ট",
|
||||
"qdrantHost": "হোস্ট",
|
||||
"qdrantCollection": "সংগ্রহ"
|
||||
"qdrantCollection": "সংগ্রহ",
|
||||
"presetAll": "All",
|
||||
"presetAllDesc": "Show everything",
|
||||
"presetEssentials": "Essentials",
|
||||
"presetEssentialsDesc": "Beginner path - Advanced tools stay searchable",
|
||||
"presetMinimal": "Minimal",
|
||||
"presetMinimalDesc": "Core pages only",
|
||||
"presetDeveloper": "Developer",
|
||||
"presetDeveloperDesc": "Dev & proxy tools",
|
||||
"presetAdmin": "Admin",
|
||||
"presetAdminDesc": "Monitoring & audit",
|
||||
"settingsSidebarTitle": "Sidebar Customization",
|
||||
"settingsSidebarDesc": "Choose which sidebar items to show. Essentials keeps Advanced tools searchable."
|
||||
},
|
||||
"contextRtk": {
|
||||
"title": "RTK Engine",
|
||||
@@ -12017,7 +12076,8 @@
|
||||
"title": "ACP এজেন্ট",
|
||||
"phrase": "CLI যা OmniRoute এক্সিকিউশন ব্যাকএন্ড হিসেবে স্পন করে (রিভার্স ফ্লো)",
|
||||
"flow": "ক্লায়েন্ট → OmniRoute → spawn CLI (stdio/ACP) → রেসপন্স",
|
||||
"seeOther": "দেখুন →"
|
||||
"seeOther": "দেখুন →",
|
||||
"warning": "Most users can ignore this — use only when an integration requires it."
|
||||
}
|
||||
},
|
||||
"comparison": {
|
||||
@@ -12841,6 +12901,10 @@
|
||||
},
|
||||
"SKILLS_SANDBOX_NETWORK_ENABLED": {
|
||||
"description": "স্কিল স্যান্ডবক্সে নেটওয়ার্ক অ্যাক্সেস সক্ষম করুন।"
|
||||
},
|
||||
"DISABLE_CONTEXT_WINDOW_CHECKS": {
|
||||
"label": "Disable Context Window Checks",
|
||||
"description": "Skip OmniRoute's local context-window and max-input-token check for direct single-model requests. Upstream providers still enforce their actual limits. Prompt compression and output-token caps remain active."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -13413,6 +13477,13 @@
|
||||
"modelLockouts": "মডেল লকআউট",
|
||||
"count": "সংযোগ সংখ্যা"
|
||||
}
|
||||
},
|
||||
"reassuranceTitle": "Your connections recover automatically",
|
||||
"reassuranceDetail": "Usually no action is needed. OmniRoute temporarily rests a connection after failures, then safely tries it again.",
|
||||
"plainStates": {
|
||||
"healthy": "Requests can be sent",
|
||||
"coolingDown": "Trying again soon",
|
||||
"lockedOut": "Needs your attention"
|
||||
}
|
||||
},
|
||||
"featureFlagCapabilityFilterEnabledDescription": "লক্ষ্য মডেলের প্রয়োজনীয় সক্ষমতা (দৃষ্টি, সরঞ্জাম, কাঠামোবদ্ধ আউটপুট, প্রসঙ্গ উইন্ডো) অনুপস্থিত থাকলে প্রেরণের আগে অনুরোধগুলি প্রত্যাখ্যান করুন। এটি কম্বো-লেয়ার সামঞ্জস্য ফিল্টারকে বাইপাস করা সরাসরি একক-প্রদানকারী অনুরোধগুলি রক্ষা করে।",
|
||||
@@ -13850,5 +13921,13 @@
|
||||
"toolsMismatch": "প্রদানকারী টুল কলিং সমর্থন করে না",
|
||||
"structuredOutputMismatch": "প্রোভাইডার স্ট্রাকচারড আউটপুট সমর্থন করে না",
|
||||
"contextWindowMismatch": "অনুরোধটি প্রদানকারীর প্রসঙ্গ উইন্ডো অতিক্রম করেছে"
|
||||
},
|
||||
"featureFlagDisableContextWindowChecksDescription": "Skip OmniRoute's local context-window and max-input-token check for direct single-model requests. Upstream providers still enforce their actual limits. Prompt compression and output-token caps remain active.",
|
||||
"cheaperInferenceSponsorBanner": {
|
||||
"title": "Cheaper Inference is an OmniRoute Open Source Friend",
|
||||
"description": "A cost-ranked gateway reselling dozens of frontier models behind one OpenAI-compatible endpoint — routing each request to the cheapest eligible provider, never above list price.",
|
||||
"cta": "Get an API Key",
|
||||
"partnerLinkNote": "Partner link",
|
||||
"dismissAriaLabel": "Dismiss"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -974,7 +974,14 @@
|
||||
"batchFileUsedByCount": "Použito {count, plural, one {# dávka} other {# dávky}}",
|
||||
"batchFilePreview": "Náhled",
|
||||
"batchFilePreviewTruncated": "Zobrazuji prvních {shown} řádků ({total} celkem řádků)",
|
||||
"batchFileDownloadFull": "Stáhnout celý soubor"
|
||||
"batchFileDownloadFull": "Stáhnout celý soubor",
|
||||
"batchHeaderSubtitle": "Run many requests as one job",
|
||||
"batchStep1": "1 · Upload JSONL",
|
||||
"batchStep1Desc": "Add requests",
|
||||
"batchStep2": "2 · Create batch",
|
||||
"batchStep2Desc": "Run job",
|
||||
"batchStep3": "3 · Get results",
|
||||
"batchStep3Desc": "Download output"
|
||||
},
|
||||
"disabled": "Zakázáno",
|
||||
"featureFlagOmnirouteEmergencyFallbackDescription": "Směrovat požadavky s vyčerpaným rozpočtem na nouzového bezplatného záložního poskytovatele/model.",
|
||||
@@ -1293,7 +1300,8 @@
|
||||
"open": "otevřít",
|
||||
"close": "zavřít"
|
||||
},
|
||||
"noResults": "Žádné výsledky"
|
||||
"noResults": "Žádné výsledky",
|
||||
"trafficInspectorPurpose": "See exactly what your application sends to and receives from AI providers. Works with any OpenAI-compatible client."
|
||||
},
|
||||
"webhooks": {
|
||||
"title": "Webhooky",
|
||||
@@ -1856,7 +1864,21 @@
|
||||
"directDownloadHint": "Nebo stáhněte příslušný formát instalátoru přímo:",
|
||||
"releaseNotes": "Poznámky k vydání",
|
||||
"readMore": "Přečíst více",
|
||||
"noAuthLabel": "Žádná autentizace"
|
||||
"noAuthLabel": "Žádná autentizace",
|
||||
"recentRequests": "Recent Requests",
|
||||
"recentRequestsEmpty": "No requests yet.",
|
||||
"recentRequestsModel": "Model",
|
||||
"recentRequestsTokens": "In / Out",
|
||||
"recentRequestsWhen": "When",
|
||||
"readinessEyebrow": "Get ready to route",
|
||||
"readinessTitle": "Send your first request",
|
||||
"readinessSubtitle": "Four small steps. OmniRoute checks readiness as you go.",
|
||||
"readinessStep1": "Connect a provider",
|
||||
"readinessStep2": "Configure endpoint authentication",
|
||||
"readinessStep3": "Copy your endpoint",
|
||||
"readinessStep4": "Send a test request",
|
||||
"readinessContinue": "Continue setup",
|
||||
"readinessDismiss": "Dismiss for now"
|
||||
},
|
||||
"analytics": {
|
||||
"title": "Analytika",
|
||||
@@ -2901,7 +2923,8 @@
|
||||
"omp": "Terminálový programovací agent Oh My Pi",
|
||||
"letta": "CLI agent Letta s trvalou pamětí a používáním nástrojů",
|
||||
"warp": "AI terminál Warp s podporou vlastních poskytovatelů",
|
||||
"agent-deck": "Multiagentní orchestrátor Agent Deck"
|
||||
"agent-deck": "Multiagentní orchestrátor Agent Deck",
|
||||
"prime-agent": "Prime Agent — self-improving RLM coding harness with OpenAI-compatible provider support"
|
||||
},
|
||||
"guides": {
|
||||
"cursor": {
|
||||
@@ -4133,7 +4156,10 @@
|
||||
"notionIntegrationHelp": "Vytvořte interní integraci na",
|
||||
"notionIntegrationToken": "Notion Interní Integrační Token",
|
||||
"notionNotConnected": "Nepřipojeno",
|
||||
"notionTokenConfigured": "Token byl nakonfigurován. Nástroje Notion jsou k dispozici prostřednictvím MCP."
|
||||
"notionTokenConfigured": "Token byl nakonfigurován. Nástroje Notion jsou k dispozici prostřednictvím MCP.",
|
||||
"subtitle": "Use the OpenAI-compatible endpoint with most SDKs and tools.",
|
||||
"testEndpoint": "Test endpoint →",
|
||||
"advancedProtocols": "Advanced protocols"
|
||||
},
|
||||
"endpoints": {
|
||||
"tabProxy": "Koncová Proxy",
|
||||
@@ -4716,7 +4742,14 @@
|
||||
"issueCount": "{count} problémů",
|
||||
"score": "Skóre",
|
||||
"lastRequest": "Poslední požadavek",
|
||||
"lastError": "Poslední chyba"
|
||||
"lastError": "Poslední chyba",
|
||||
"healthVerdictReady": "OmniRoute is ready",
|
||||
"healthVerdictActionRequired": "Action required to restore full operation",
|
||||
"healthVerdictCoolingDown": "Cooling down after recent changes",
|
||||
"healthSubtitle": "System health check",
|
||||
"advancedDiagnosticsTitle": "Advanced diagnostics",
|
||||
"hide": "Hide",
|
||||
"show": "Show"
|
||||
},
|
||||
"telemetry": {
|
||||
"title": "Systémová telemetrie",
|
||||
@@ -6383,7 +6416,21 @@
|
||||
"savedModelEndpointSettings": "Nastavení koncového bodu uloženého modelu",
|
||||
"searchByModelAria": "Hledat podle modelu",
|
||||
"selectSupportedEndpoint": "Vyberte alespoň jeden podporovaný koncový bod",
|
||||
"antigravityClientProfileHarness": "Harness / CLI"
|
||||
"antigravityClientProfileHarness": "Harness / CLI",
|
||||
"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."
|
||||
},
|
||||
"settings": {
|
||||
"title": "Nastavení",
|
||||
@@ -8230,7 +8277,19 @@
|
||||
"cliproxyapiHealth": "Zdraví",
|
||||
"cliproxyapiPort": "Port",
|
||||
"qdrantHost": "Host",
|
||||
"qdrantCollection": "Kolekce"
|
||||
"qdrantCollection": "Kolekce",
|
||||
"presetAll": "All",
|
||||
"presetAllDesc": "Show everything",
|
||||
"presetEssentials": "Essentials",
|
||||
"presetEssentialsDesc": "Beginner path - Advanced tools stay searchable",
|
||||
"presetMinimal": "Minimal",
|
||||
"presetMinimalDesc": "Core pages only",
|
||||
"presetDeveloper": "Developer",
|
||||
"presetDeveloperDesc": "Dev & proxy tools",
|
||||
"presetAdmin": "Admin",
|
||||
"presetAdminDesc": "Monitoring & audit",
|
||||
"settingsSidebarTitle": "Sidebar Customization",
|
||||
"settingsSidebarDesc": "Choose which sidebar items to show. Essentials keeps Advanced tools searchable."
|
||||
},
|
||||
"contextRtk": {
|
||||
"title": "RTK Engine",
|
||||
@@ -12017,7 +12076,8 @@
|
||||
"title": "ACP Agents",
|
||||
"phrase": "Rozhraní CLI, která OmniRoute spouští jako prováděcí backend (zpětný tok)",
|
||||
"flow": "Klient → OmniRoute → spustit CLI (stdio/ACP) → odpověď",
|
||||
"seeOther": "Zobrazit →"
|
||||
"seeOther": "Zobrazit →",
|
||||
"warning": "Most users can ignore this — use only when an integration requires it."
|
||||
}
|
||||
},
|
||||
"comparison": {
|
||||
@@ -12841,6 +12901,10 @@
|
||||
},
|
||||
"SKILLS_SANDBOX_NETWORK_ENABLED": {
|
||||
"description": "Povolit přístup k síti v sandboxu dovedností."
|
||||
},
|
||||
"DISABLE_CONTEXT_WINDOW_CHECKS": {
|
||||
"label": "Disable Context Window Checks",
|
||||
"description": "Skip OmniRoute's local context-window and max-input-token check for direct single-model requests. Upstream providers still enforce their actual limits. Prompt compression and output-token caps remain active."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -13413,6 +13477,13 @@
|
||||
"modelLockouts": "Uzamčení Modelu",
|
||||
"count": "Počet Připojení"
|
||||
}
|
||||
},
|
||||
"reassuranceTitle": "Your connections recover automatically",
|
||||
"reassuranceDetail": "Usually no action is needed. OmniRoute temporarily rests a connection after failures, then safely tries it again.",
|
||||
"plainStates": {
|
||||
"healthy": "Requests can be sent",
|
||||
"coolingDown": "Trying again soon",
|
||||
"lockedOut": "Needs your attention"
|
||||
}
|
||||
},
|
||||
"featureFlagCapabilityFilterEnabledDescription": "Odmítnout požadavky před odesláním, když cílový model postrádá požadované schopnosti (vidění, nástroje, strukturovaný výstup, kontextové okno). Chrání přímé požadavky od jednotlivých poskytovatelů, které obcházejí filtr kompatibility kombinované vrstvy.",
|
||||
@@ -13850,5 +13921,13 @@
|
||||
"toolsMismatch": "Poskytovatel nepodporuje volání nástroje",
|
||||
"structuredOutputMismatch": "Poskytovatel nepodporuje strukturovaný výstup",
|
||||
"contextWindowMismatch": "Žádost překračuje kontextové okno poskytovatele"
|
||||
},
|
||||
"featureFlagDisableContextWindowChecksDescription": "Skip OmniRoute's local context-window and max-input-token check for direct single-model requests. Upstream providers still enforce their actual limits. Prompt compression and output-token caps remain active.",
|
||||
"cheaperInferenceSponsorBanner": {
|
||||
"title": "Cheaper Inference is an OmniRoute Open Source Friend",
|
||||
"description": "A cost-ranked gateway reselling dozens of frontier models behind one OpenAI-compatible endpoint — routing each request to the cheapest eligible provider, never above list price.",
|
||||
"cta": "Get an API Key",
|
||||
"partnerLinkNote": "Partner link",
|
||||
"dismissAriaLabel": "Dismiss"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -974,7 +974,14 @@
|
||||
"batchFileUsedByCount": "Brugt af {count, plural, one {# batch} other {# batches}}",
|
||||
"batchFilePreview": "Forhåndsvisning",
|
||||
"batchFilePreviewTruncated": "Viser de første {shown} linjer ({total} linjer i alt)",
|
||||
"batchFileDownloadFull": "Download Fuld Fil"
|
||||
"batchFileDownloadFull": "Download Fuld Fil",
|
||||
"batchHeaderSubtitle": "Run many requests as one job",
|
||||
"batchStep1": "1 · Upload JSONL",
|
||||
"batchStep1Desc": "Add requests",
|
||||
"batchStep2": "2 · Create batch",
|
||||
"batchStep2Desc": "Run job",
|
||||
"batchStep3": "3 · Get results",
|
||||
"batchStep3Desc": "Download output"
|
||||
},
|
||||
"disabled": "Deaktiveret",
|
||||
"featureFlagOmnirouteEmergencyFallbackDescription": "Diriger budgetudtømte anmodninger til den gratis nød-fallback-udbyder/-model.",
|
||||
@@ -1293,7 +1300,8 @@
|
||||
"open": "åben",
|
||||
"close": "luk"
|
||||
},
|
||||
"noResults": "Ingen resultater"
|
||||
"noResults": "Ingen resultater",
|
||||
"trafficInspectorPurpose": "See exactly what your application sends to and receives from AI providers. Works with any OpenAI-compatible client."
|
||||
},
|
||||
"webhooks": {
|
||||
"title": "Webhooks",
|
||||
@@ -1856,7 +1864,21 @@
|
||||
"directDownloadHint": "Eller download det respektive installationsformat direkte:",
|
||||
"releaseNotes": "Udgivelsesnoter",
|
||||
"readMore": "Læs Mere",
|
||||
"noAuthLabel": "Ingen godkendelse"
|
||||
"noAuthLabel": "Ingen godkendelse",
|
||||
"recentRequests": "Recent Requests",
|
||||
"recentRequestsEmpty": "No requests yet.",
|
||||
"recentRequestsModel": "Model",
|
||||
"recentRequestsTokens": "In / Out",
|
||||
"recentRequestsWhen": "When",
|
||||
"readinessEyebrow": "Get ready to route",
|
||||
"readinessTitle": "Send your first request",
|
||||
"readinessSubtitle": "Four small steps. OmniRoute checks readiness as you go.",
|
||||
"readinessStep1": "Connect a provider",
|
||||
"readinessStep2": "Configure endpoint authentication",
|
||||
"readinessStep3": "Copy your endpoint",
|
||||
"readinessStep4": "Send a test request",
|
||||
"readinessContinue": "Continue setup",
|
||||
"readinessDismiss": "Dismiss for now"
|
||||
},
|
||||
"analytics": {
|
||||
"title": "Analytics",
|
||||
@@ -2901,7 +2923,8 @@
|
||||
"omp": "Oh My Pi terminal-kodningsagent",
|
||||
"letta": "Letta CLI-agent med persistent hukommelse og brug af værktøjer",
|
||||
"warp": "Warp AI-terminal med understøttelse af brugerdefineret udbyder",
|
||||
"agent-deck": "Agent Deck multi-agent-orkestrator"
|
||||
"agent-deck": "Agent Deck multi-agent-orkestrator",
|
||||
"prime-agent": "Prime Agent — self-improving RLM coding harness with OpenAI-compatible provider support"
|
||||
},
|
||||
"guides": {
|
||||
"cursor": {
|
||||
@@ -4133,7 +4156,10 @@
|
||||
"notionIntegrationHelp": "Opret en intern integration ved",
|
||||
"notionIntegrationToken": "Notion Intern Token til Integration",
|
||||
"notionNotConnected": "Ikke tilsluttet",
|
||||
"notionTokenConfigured": "Token konfigureret. Notion-værktøjer er tilgængelige via MCP."
|
||||
"notionTokenConfigured": "Token konfigureret. Notion-værktøjer er tilgængelige via MCP.",
|
||||
"subtitle": "Use the OpenAI-compatible endpoint with most SDKs and tools.",
|
||||
"testEndpoint": "Test endpoint →",
|
||||
"advancedProtocols": "Advanced protocols"
|
||||
},
|
||||
"endpoints": {
|
||||
"tabProxy": "Endpoint Proxy",
|
||||
@@ -4716,7 +4742,14 @@
|
||||
"issueCount": "{count} problemer",
|
||||
"score": "Score",
|
||||
"lastRequest": "Sidste anmodning",
|
||||
"lastError": "Sidste fejl"
|
||||
"lastError": "Sidste fejl",
|
||||
"healthVerdictReady": "OmniRoute is ready",
|
||||
"healthVerdictActionRequired": "Action required to restore full operation",
|
||||
"healthVerdictCoolingDown": "Cooling down after recent changes",
|
||||
"healthSubtitle": "System health check",
|
||||
"advancedDiagnosticsTitle": "Advanced diagnostics",
|
||||
"hide": "Hide",
|
||||
"show": "Show"
|
||||
},
|
||||
"telemetry": {
|
||||
"title": "System telemetri",
|
||||
@@ -6383,7 +6416,21 @@
|
||||
"savedModelEndpointSettings": "Indstillinger for gemt model endpoint",
|
||||
"searchByModelAria": "Søg efter model",
|
||||
"selectSupportedEndpoint": "Vælg mindst én understøttet endpoint",
|
||||
"antigravityClientProfileHarness": "Harness / CLI"
|
||||
"antigravityClientProfileHarness": "Harness / CLI",
|
||||
"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."
|
||||
},
|
||||
"settings": {
|
||||
"title": "Indstillinger",
|
||||
@@ -8230,7 +8277,19 @@
|
||||
"cliproxyapiHealth": "Sundhed",
|
||||
"cliproxyapiPort": "Port",
|
||||
"qdrantHost": "Vært",
|
||||
"qdrantCollection": "Samling"
|
||||
"qdrantCollection": "Samling",
|
||||
"presetAll": "All",
|
||||
"presetAllDesc": "Show everything",
|
||||
"presetEssentials": "Essentials",
|
||||
"presetEssentialsDesc": "Beginner path - Advanced tools stay searchable",
|
||||
"presetMinimal": "Minimal",
|
||||
"presetMinimalDesc": "Core pages only",
|
||||
"presetDeveloper": "Developer",
|
||||
"presetDeveloperDesc": "Dev & proxy tools",
|
||||
"presetAdmin": "Admin",
|
||||
"presetAdminDesc": "Monitoring & audit",
|
||||
"settingsSidebarTitle": "Sidebar Customization",
|
||||
"settingsSidebarDesc": "Choose which sidebar items to show. Essentials keeps Advanced tools searchable."
|
||||
},
|
||||
"contextRtk": {
|
||||
"title": "RTK Engine",
|
||||
@@ -12017,7 +12076,8 @@
|
||||
"title": "ACP-agenter",
|
||||
"phrase": "CLI'er, som OmniRoute starter som eksekveringsbackend (omvendt flow)",
|
||||
"flow": "Klient → OmniRoute → spawn CLI (stdio/ACP) → svar",
|
||||
"seeOther": "Se →"
|
||||
"seeOther": "Se →",
|
||||
"warning": "Most users can ignore this — use only when an integration requires it."
|
||||
}
|
||||
},
|
||||
"comparison": {
|
||||
@@ -12841,6 +12901,10 @@
|
||||
},
|
||||
"SKILLS_SANDBOX_NETWORK_ENABLED": {
|
||||
"description": "Aktivér netværksadgang i skills-sandkassen."
|
||||
},
|
||||
"DISABLE_CONTEXT_WINDOW_CHECKS": {
|
||||
"label": "Disable Context Window Checks",
|
||||
"description": "Skip OmniRoute's local context-window and max-input-token check for direct single-model requests. Upstream providers still enforce their actual limits. Prompt compression and output-token caps remain active."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -13413,6 +13477,13 @@
|
||||
"modelLockouts": "Modellåsninger",
|
||||
"count": "Antal Forbindelser"
|
||||
}
|
||||
},
|
||||
"reassuranceTitle": "Your connections recover automatically",
|
||||
"reassuranceDetail": "Usually no action is needed. OmniRoute temporarily rests a connection after failures, then safely tries it again.",
|
||||
"plainStates": {
|
||||
"healthy": "Requests can be sent",
|
||||
"coolingDown": "Trying again soon",
|
||||
"lockedOut": "Needs your attention"
|
||||
}
|
||||
},
|
||||
"featureFlagCapabilityFilterEnabledDescription": "Afvis anmodninger før afsendelse, når målmodellen mangler de nødvendige funktioner (vision, værktøjer, struktureret output, kontekstvindue). Beskytter direkte anmodninger fra en enkelt udbyder, der omgår kombinationslagets kompatibilitetsfilter.",
|
||||
@@ -13850,5 +13921,13 @@
|
||||
"toolsMismatch": "Udbyderen understøtter ikke værktøjsopkald.",
|
||||
"structuredOutputMismatch": "Udbyderen understøtter ikke struktureret output",
|
||||
"contextWindowMismatch": "Anmodningen overskrider udbyderens kontekstvindue"
|
||||
},
|
||||
"featureFlagDisableContextWindowChecksDescription": "Skip OmniRoute's local context-window and max-input-token check for direct single-model requests. Upstream providers still enforce their actual limits. Prompt compression and output-token caps remain active.",
|
||||
"cheaperInferenceSponsorBanner": {
|
||||
"title": "Cheaper Inference is an OmniRoute Open Source Friend",
|
||||
"description": "A cost-ranked gateway reselling dozens of frontier models behind one OpenAI-compatible endpoint — routing each request to the cheapest eligible provider, never above list price.",
|
||||
"cta": "Get an API Key",
|
||||
"partnerLinkNote": "Partner link",
|
||||
"dismissAriaLabel": "Dismiss"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -974,7 +974,14 @@
|
||||
"batchFileUsedByCount": "Verwendet von {count, plural, one {# Batch} other {# Batches}}",
|
||||
"batchFilePreview": "Vorschau",
|
||||
"batchFilePreviewTruncated": "Zeige die ersten {shown} Zeilen ({total} insgesamt)",
|
||||
"batchFileDownloadFull": "Vollständige Datei herunterladen"
|
||||
"batchFileDownloadFull": "Vollständige Datei herunterladen",
|
||||
"batchHeaderSubtitle": "Run many requests as one job",
|
||||
"batchStep1": "1 · Upload JSONL",
|
||||
"batchStep1Desc": "Add requests",
|
||||
"batchStep2": "2 · Create batch",
|
||||
"batchStep2Desc": "Run job",
|
||||
"batchStep3": "3 · Get results",
|
||||
"batchStep3Desc": "Download output"
|
||||
},
|
||||
"disabled": "Deaktiviert",
|
||||
"featureFlagOmnirouteEmergencyFallbackDescription": "Anfragen mit erschöpftem Budget an den kostenlosen Notfall-Fallback-Anbieter/das Notfall-Fallback-Modell weiterleiten.",
|
||||
@@ -1293,7 +1300,8 @@
|
||||
"open": "öffnen",
|
||||
"close": "schließen"
|
||||
},
|
||||
"noResults": "Keine Ergebnisse"
|
||||
"noResults": "Keine Ergebnisse",
|
||||
"trafficInspectorPurpose": "See exactly what your application sends to and receives from AI providers. Works with any OpenAI-compatible client."
|
||||
},
|
||||
"webhooks": {
|
||||
"title": "Webhooks",
|
||||
@@ -1856,7 +1864,21 @@
|
||||
"directDownloadHint": "Oder laden Sie das jeweilige Installationsformat direkt herunter:",
|
||||
"releaseNotes": "Versionshinweise",
|
||||
"readMore": "Mehr Lesen",
|
||||
"noAuthLabel": "Keine Authentifizierung"
|
||||
"noAuthLabel": "Keine Authentifizierung",
|
||||
"recentRequests": "Recent Requests",
|
||||
"recentRequestsEmpty": "No requests yet.",
|
||||
"recentRequestsModel": "Model",
|
||||
"recentRequestsTokens": "In / Out",
|
||||
"recentRequestsWhen": "When",
|
||||
"readinessEyebrow": "Get ready to route",
|
||||
"readinessTitle": "Send your first request",
|
||||
"readinessSubtitle": "Four small steps. OmniRoute checks readiness as you go.",
|
||||
"readinessStep1": "Connect a provider",
|
||||
"readinessStep2": "Configure endpoint authentication",
|
||||
"readinessStep3": "Copy your endpoint",
|
||||
"readinessStep4": "Send a test request",
|
||||
"readinessContinue": "Continue setup",
|
||||
"readinessDismiss": "Dismiss for now"
|
||||
},
|
||||
"analytics": {
|
||||
"title": "Analytik",
|
||||
@@ -2901,7 +2923,8 @@
|
||||
"omp": "Oh My Pi Terminal-Coding-Agent",
|
||||
"letta": "Letta CLI-Agent mit persistentem Speicher und Tool-Nutzung",
|
||||
"warp": "Warp AI-Terminal mit Unterstützung für benutzerdefinierte Anbieter",
|
||||
"agent-deck": "Agent Deck Multi-Agenten-Orchestrator"
|
||||
"agent-deck": "Agent Deck Multi-Agenten-Orchestrator",
|
||||
"prime-agent": "Prime Agent — self-improving RLM coding harness with OpenAI-compatible provider support"
|
||||
},
|
||||
"guides": {
|
||||
"cursor": {
|
||||
@@ -4133,7 +4156,10 @@
|
||||
"notionIntegrationHelp": "Erstellen Sie eine interne Integration bei",
|
||||
"notionIntegrationToken": "Notion Interner Integrations-Token",
|
||||
"notionNotConnected": "Nicht verbunden",
|
||||
"notionTokenConfigured": "Token konfiguriert. Notion-Tools sind über MCP verfügbar."
|
||||
"notionTokenConfigured": "Token konfiguriert. Notion-Tools sind über MCP verfügbar.",
|
||||
"subtitle": "Use the OpenAI-compatible endpoint with most SDKs and tools.",
|
||||
"testEndpoint": "Test endpoint →",
|
||||
"advancedProtocols": "Advanced protocols"
|
||||
},
|
||||
"endpoints": {
|
||||
"tabProxy": "Endpoint Proxy",
|
||||
@@ -4716,7 +4742,14 @@
|
||||
"issueCount": "{count} Probleme",
|
||||
"score": "Score",
|
||||
"lastRequest": "Letzte Anfrage",
|
||||
"lastError": "Letzter Fehler"
|
||||
"lastError": "Letzter Fehler",
|
||||
"healthVerdictReady": "OmniRoute is ready",
|
||||
"healthVerdictActionRequired": "Action required to restore full operation",
|
||||
"healthVerdictCoolingDown": "Cooling down after recent changes",
|
||||
"healthSubtitle": "System health check",
|
||||
"advancedDiagnosticsTitle": "Advanced diagnostics",
|
||||
"hide": "Hide",
|
||||
"show": "Show"
|
||||
},
|
||||
"telemetry": {
|
||||
"title": "Systemtelemetrie",
|
||||
@@ -6383,7 +6416,21 @@
|
||||
"savedModelEndpointSettings": "Einstellungen für den gespeicherten Modell-Endpunkt",
|
||||
"searchByModelAria": "Nach Modell suchen",
|
||||
"selectSupportedEndpoint": "Wählen Sie mindestens einen unterstützten Endpunkt aus",
|
||||
"antigravityClientProfileHarness": "Harness / CLI"
|
||||
"antigravityClientProfileHarness": "Harness / CLI",
|
||||
"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."
|
||||
},
|
||||
"settings": {
|
||||
"title": "Einstellungen",
|
||||
@@ -8230,7 +8277,19 @@
|
||||
"cliproxyapiHealth": "Gesundheit",
|
||||
"cliproxyapiPort": "Port",
|
||||
"qdrantHost": "Host",
|
||||
"qdrantCollection": "Sammlung"
|
||||
"qdrantCollection": "Sammlung",
|
||||
"presetAll": "All",
|
||||
"presetAllDesc": "Show everything",
|
||||
"presetEssentials": "Essentials",
|
||||
"presetEssentialsDesc": "Beginner path - Advanced tools stay searchable",
|
||||
"presetMinimal": "Minimal",
|
||||
"presetMinimalDesc": "Core pages only",
|
||||
"presetDeveloper": "Developer",
|
||||
"presetDeveloperDesc": "Dev & proxy tools",
|
||||
"presetAdmin": "Admin",
|
||||
"presetAdminDesc": "Monitoring & audit",
|
||||
"settingsSidebarTitle": "Sidebar Customization",
|
||||
"settingsSidebarDesc": "Choose which sidebar items to show. Essentials keeps Advanced tools searchable."
|
||||
},
|
||||
"contextRtk": {
|
||||
"title": "RTK Engine",
|
||||
@@ -12017,7 +12076,8 @@
|
||||
"title": "ACP-Agenten",
|
||||
"phrase": "CLIs, die OmniRoute als Ausführungs-Backend startet (umgekehrter Fluss)",
|
||||
"flow": "Client → OmniRoute → CLI starten (stdio/ACP) → Antwort",
|
||||
"seeOther": "Siehe →"
|
||||
"seeOther": "Siehe →",
|
||||
"warning": "Most users can ignore this — use only when an integration requires it."
|
||||
}
|
||||
},
|
||||
"comparison": {
|
||||
@@ -13417,6 +13477,13 @@
|
||||
"modelLockouts": "Modellsperren",
|
||||
"count": "Verbindungsanzahl"
|
||||
}
|
||||
},
|
||||
"reassuranceTitle": "Your connections recover automatically",
|
||||
"reassuranceDetail": "Usually no action is needed. OmniRoute temporarily rests a connection after failures, then safely tries it again.",
|
||||
"plainStates": {
|
||||
"healthy": "Requests can be sent",
|
||||
"coolingDown": "Trying again soon",
|
||||
"lockedOut": "Needs your attention"
|
||||
}
|
||||
},
|
||||
"featureFlagCapabilityFilterEnabledDescription": "Lehnen Sie Anfragen ab, bevor sie versendet werden, wenn das Zielmodell über die erforderlichen Funktionen (Vision, Werkzeuge, strukturierte Ausgabe, Kontextfenster) nicht verfügt. Schützt direkte Einzelanbieteranfragen, die den Kombo-Schicht-Kompatibilitätsfilter umgehen.",
|
||||
@@ -13855,5 +13922,12 @@
|
||||
"toolsMismatch": "Der Anbieter unterstützt keinen Toolaufruf",
|
||||
"structuredOutputMismatch": "Der Anbieter unterstützt keine strukturierten Ausgaben",
|
||||
"contextWindowMismatch": "Anfrage überschreitet das Kontextfenster des Anbieters"
|
||||
},
|
||||
"cheaperInferenceSponsorBanner": {
|
||||
"title": "Cheaper Inference is an OmniRoute Open Source Friend",
|
||||
"description": "A cost-ranked gateway reselling dozens of frontier models behind one OpenAI-compatible endpoint — routing each request to the cheapest eligible provider, never above list price.",
|
||||
"cta": "Get an API Key",
|
||||
"partnerLinkNote": "Partner link",
|
||||
"dismissAriaLabel": "Dismiss"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1221,12 +1221,6 @@
|
||||
"consoleLogsSubtitle": "Console output",
|
||||
"logsActivitySubtitle": "User activity log",
|
||||
"healthSubtitle": "System health check",
|
||||
"healthVerdictReady": "OmniRoute is ready",
|
||||
"healthVerdictActionRequired": "Action required to restore full operation",
|
||||
"healthVerdictCoolingDown": "Cooling down after recent changes",
|
||||
"advancedDiagnosticsTitle": "Advanced diagnostics",
|
||||
"hide": "Hide",
|
||||
"show": "Show",
|
||||
"costsPricingSubtitle": "Per-model pricing rules",
|
||||
"costsBudgetSubtitle": "Budget limits",
|
||||
"costsQuotaShareSubtitle": "Share provider quotas across keys",
|
||||
@@ -1272,7 +1266,8 @@
|
||||
"agentBridge": "Agent Bridge",
|
||||
"agentBridgeSubtitle": "Intercept IDE agent traffic",
|
||||
"trafficInspector": "Traffic Inspector",
|
||||
"trafficInspectorSubtitle": "Monitor LLM calls + debug any HTTPS traffic",
|
||||
"trafficInspectorSubtitle": "Inspect request and response traffic from your apps",
|
||||
"trafficInspectorPurpose": "See exactly what your application sends to and receives from AI providers. Works with any OpenAI-compatible client.",
|
||||
"cliCode": "CLI Code",
|
||||
"cliCodeSubtitle": "Code tools pointing to OmniRoute",
|
||||
"cliAgents": "CLI Agents",
|
||||
@@ -2927,6 +2922,7 @@
|
||||
"interpreter": "Open Interpreter autonomous coding agent CLI",
|
||||
"omp": "Oh My Pi terminal coding agent",
|
||||
"letta": "Letta CLI agent with persistent memory and tool use",
|
||||
"prime-agent": "Prime Agent — self-improving RLM coding harness with OpenAI-compatible provider support",
|
||||
"warp": "Warp AI terminal with custom provider support",
|
||||
"agent-deck": "Agent Deck multi-agent orchestrator"
|
||||
},
|
||||
@@ -4636,6 +4632,13 @@
|
||||
"retry": "Retry",
|
||||
"allOperational": "All systems operational",
|
||||
"issuesDetected": "System issues detected",
|
||||
"healthVerdictReady": "OmniRoute is ready",
|
||||
"healthVerdictActionRequired": "Action required to restore full operation",
|
||||
"healthVerdictCoolingDown": "Cooling down after recent changes",
|
||||
"healthSubtitle": "System health check",
|
||||
"advancedDiagnosticsTitle": "Advanced diagnostics",
|
||||
"hide": "Hide",
|
||||
"show": "Show",
|
||||
"updatedAt": "Updated {time}",
|
||||
"latency": "Latency",
|
||||
"latencyP50": "p50",
|
||||
@@ -6707,6 +6710,18 @@
|
||||
"sidebarVisibility": "Hide sidebar items",
|
||||
"sidebarVisibilityDesc": "Hide any sidebar navigation entry to reduce visual clutter without disabling any features",
|
||||
"sidebarVisibilityHint": "Any sidebar section is hidden automatically when all of its entries are hidden",
|
||||
"presetAll": "All",
|
||||
"presetAllDesc": "Show everything",
|
||||
"presetEssentials": "Essentials",
|
||||
"presetEssentialsDesc": "Beginner path - Advanced tools stay searchable",
|
||||
"presetMinimal": "Minimal",
|
||||
"presetMinimalDesc": "Core pages only",
|
||||
"presetDeveloper": "Developer",
|
||||
"presetDeveloperDesc": "Dev & proxy tools",
|
||||
"presetAdmin": "Admin",
|
||||
"presetAdminDesc": "Monitoring & audit",
|
||||
"settingsSidebarTitle": "Sidebar Customization",
|
||||
"settingsSidebarDesc": "Choose which sidebar items to show. Essentials keeps Advanced tools searchable.",
|
||||
"hideHealthLogs": "Hide Health Check Logs",
|
||||
"hideHealthLogsDesc": "When ON, suppress [HealthCheck] messages in server console",
|
||||
"themeAccent": "Theme color",
|
||||
|
||||
@@ -974,7 +974,14 @@
|
||||
"batchFileUsedByCount": "Usado por {count, plural, one {# lote} other {# lotes}}",
|
||||
"batchFilePreview": "Vista Previa",
|
||||
"batchFilePreviewTruncated": "Mostrando las primeras {shown} líneas ({total} líneas en total)",
|
||||
"batchFileDownloadFull": "Descargar Archivo Completo"
|
||||
"batchFileDownloadFull": "Descargar Archivo Completo",
|
||||
"batchHeaderSubtitle": "Run many requests as one job",
|
||||
"batchStep1": "1 · Upload JSONL",
|
||||
"batchStep1Desc": "Add requests",
|
||||
"batchStep2": "2 · Create batch",
|
||||
"batchStep2Desc": "Run job",
|
||||
"batchStep3": "3 · Get results",
|
||||
"batchStep3Desc": "Download output"
|
||||
},
|
||||
"disabled": "Disabled",
|
||||
"featureFlagOmnirouteEmergencyFallbackDescription": "Route budget-exhausted requests to the emergency free fallback provider/model.",
|
||||
@@ -1293,7 +1300,8 @@
|
||||
"open": "abrir",
|
||||
"close": "cerrar"
|
||||
},
|
||||
"noResults": "Sin resultados"
|
||||
"noResults": "Sin resultados",
|
||||
"trafficInspectorPurpose": "See exactly what your application sends to and receives from AI providers. Works with any OpenAI-compatible client."
|
||||
},
|
||||
"webhooks": {
|
||||
"title": "Ganchos web",
|
||||
@@ -1856,7 +1864,21 @@
|
||||
"directDownloadHint": "O descarga el formato de instalador respectivo directamente:",
|
||||
"releaseNotes": "Notas de la versión",
|
||||
"readMore": "Leer Más",
|
||||
"noAuthLabel": "Sin Autenticación"
|
||||
"noAuthLabel": "Sin Autenticación",
|
||||
"recentRequests": "Recent Requests",
|
||||
"recentRequestsEmpty": "No requests yet.",
|
||||
"recentRequestsModel": "Model",
|
||||
"recentRequestsTokens": "In / Out",
|
||||
"recentRequestsWhen": "When",
|
||||
"readinessEyebrow": "Get ready to route",
|
||||
"readinessTitle": "Send your first request",
|
||||
"readinessSubtitle": "Four small steps. OmniRoute checks readiness as you go.",
|
||||
"readinessStep1": "Connect a provider",
|
||||
"readinessStep2": "Configure endpoint authentication",
|
||||
"readinessStep3": "Copy your endpoint",
|
||||
"readinessStep4": "Send a test request",
|
||||
"readinessContinue": "Continue setup",
|
||||
"readinessDismiss": "Dismiss for now"
|
||||
},
|
||||
"analytics": {
|
||||
"title": "Analítica",
|
||||
@@ -2901,7 +2923,8 @@
|
||||
"omp": "Oh My Pi terminal coding agent",
|
||||
"letta": "Letta CLI agent with persistent memory and tool use",
|
||||
"warp": "Warp AI terminal with custom provider support",
|
||||
"agent-deck": "Agent Deck multi-agent orchestrator"
|
||||
"agent-deck": "Agent Deck multi-agent orchestrator",
|
||||
"prime-agent": "Prime Agent — self-improving RLM coding harness with OpenAI-compatible provider support"
|
||||
},
|
||||
"guides": {
|
||||
"cursor": {
|
||||
@@ -4133,7 +4156,10 @@
|
||||
"notionIntegrationHelp": "Crear una Integración Interna en",
|
||||
"notionIntegrationToken": "Token de Integración Interna de Notion",
|
||||
"notionNotConnected": "No conectado",
|
||||
"notionTokenConfigured": "Token configurado. Las herramientas de Notion están disponibles a través de MCP."
|
||||
"notionTokenConfigured": "Token configurado. Las herramientas de Notion están disponibles a través de MCP.",
|
||||
"subtitle": "Use the OpenAI-compatible endpoint with most SDKs and tools.",
|
||||
"testEndpoint": "Test endpoint →",
|
||||
"advancedProtocols": "Advanced protocols"
|
||||
},
|
||||
"endpoints": {
|
||||
"tabProxy": "Endpoint Proxy",
|
||||
@@ -4716,7 +4742,14 @@
|
||||
"issueCount": "{count} issues",
|
||||
"score": "Score",
|
||||
"lastRequest": "Last request",
|
||||
"lastError": "Último error"
|
||||
"lastError": "Último error",
|
||||
"healthVerdictReady": "OmniRoute is ready",
|
||||
"healthVerdictActionRequired": "Action required to restore full operation",
|
||||
"healthVerdictCoolingDown": "Cooling down after recent changes",
|
||||
"healthSubtitle": "System health check",
|
||||
"advancedDiagnosticsTitle": "Advanced diagnostics",
|
||||
"hide": "Hide",
|
||||
"show": "Show"
|
||||
},
|
||||
"telemetry": {
|
||||
"title": "Telemetría del sistema",
|
||||
@@ -6383,7 +6416,21 @@
|
||||
"savedModelEndpointSettings": "Configuración del punto final del modelo guardado",
|
||||
"searchByModelAria": "Buscar por modelo",
|
||||
"selectSupportedEndpoint": "Seleccione al menos un endpoint compatible",
|
||||
"antigravityClientProfileHarness": "Harness / CLI"
|
||||
"antigravityClientProfileHarness": "Harness / CLI",
|
||||
"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."
|
||||
},
|
||||
"settings": {
|
||||
"title": "Configuración",
|
||||
@@ -8230,7 +8277,19 @@
|
||||
"cliproxyapiHealth": "Salud",
|
||||
"cliproxyapiPort": "Puerto",
|
||||
"qdrantHost": "Anfitrión",
|
||||
"qdrantCollection": "Colección"
|
||||
"qdrantCollection": "Colección",
|
||||
"presetAll": "All",
|
||||
"presetAllDesc": "Show everything",
|
||||
"presetEssentials": "Essentials",
|
||||
"presetEssentialsDesc": "Beginner path - Advanced tools stay searchable",
|
||||
"presetMinimal": "Minimal",
|
||||
"presetMinimalDesc": "Core pages only",
|
||||
"presetDeveloper": "Developer",
|
||||
"presetDeveloperDesc": "Dev & proxy tools",
|
||||
"presetAdmin": "Admin",
|
||||
"presetAdminDesc": "Monitoring & audit",
|
||||
"settingsSidebarTitle": "Sidebar Customization",
|
||||
"settingsSidebarDesc": "Choose which sidebar items to show. Essentials keeps Advanced tools searchable."
|
||||
},
|
||||
"contextRtk": {
|
||||
"title": "RTK Engine",
|
||||
@@ -12017,7 +12076,8 @@
|
||||
"title": "ACP Agents",
|
||||
"phrase": "CLIs that OmniRoute spawns as execution backend (reverse flow)",
|
||||
"flow": "Client → OmniRoute → spawn CLI (stdio/ACP) → response",
|
||||
"seeOther": "See →"
|
||||
"seeOther": "See →",
|
||||
"warning": "Most users can ignore this — use only when an integration requires it."
|
||||
}
|
||||
},
|
||||
"comparison": {
|
||||
@@ -12841,6 +12901,10 @@
|
||||
},
|
||||
"SKILLS_SANDBOX_NETWORK_ENABLED": {
|
||||
"description": "Enable network access in the skills sandbox."
|
||||
},
|
||||
"DISABLE_CONTEXT_WINDOW_CHECKS": {
|
||||
"label": "Disable Context Window Checks",
|
||||
"description": "Skip OmniRoute's local context-window and max-input-token check for direct single-model requests. Upstream providers still enforce their actual limits. Prompt compression and output-token caps remain active."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -13413,6 +13477,13 @@
|
||||
"modelLockouts": "Bloqueos de Modelo",
|
||||
"count": "Cantidad de Conexiones"
|
||||
}
|
||||
},
|
||||
"reassuranceTitle": "Your connections recover automatically",
|
||||
"reassuranceDetail": "Usually no action is needed. OmniRoute temporarily rests a connection after failures, then safely tries it again.",
|
||||
"plainStates": {
|
||||
"healthy": "Requests can be sent",
|
||||
"coolingDown": "Trying again soon",
|
||||
"lockedOut": "Needs your attention"
|
||||
}
|
||||
},
|
||||
"featureFlagCapabilityFilterEnabledDescription": "Rechazar solicitudes antes del despacho cuando el modelo objetivo carece de capacidades requeridas (visión, herramientas, salida estructurada, ventana de contexto). Protege las solicitudes directas de un solo proveedor que eluden el filtro de compatibilidad de la capa combinada.",
|
||||
@@ -13850,5 +13921,13 @@
|
||||
"toolsMismatch": "El proveedor no admite la llamada a la herramienta",
|
||||
"structuredOutputMismatch": "El proveedor no admite salida estructurada",
|
||||
"contextWindowMismatch": "La solicitud excede la ventana de contexto del proveedor"
|
||||
},
|
||||
"featureFlagDisableContextWindowChecksDescription": "Skip OmniRoute's local context-window and max-input-token check for direct single-model requests. Upstream providers still enforce their actual limits. Prompt compression and output-token caps remain active.",
|
||||
"cheaperInferenceSponsorBanner": {
|
||||
"title": "Cheaper Inference is an OmniRoute Open Source Friend",
|
||||
"description": "A cost-ranked gateway reselling dozens of frontier models behind one OpenAI-compatible endpoint — routing each request to the cheapest eligible provider, never above list price.",
|
||||
"cta": "Get an API Key",
|
||||
"partnerLinkNote": "Partner link",
|
||||
"dismissAriaLabel": "Dismiss"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -974,7 +974,14 @@
|
||||
"batchFileUsedByCount": "استفاده شده توسط {count, plural, one {# دسته} other {# دستهها}}",
|
||||
"batchFilePreview": "پیشنمایش",
|
||||
"batchFilePreviewTruncated": "نمایش {shown} خط اول ({total} خط کل)",
|
||||
"batchFileDownloadFull": "دانلود فایل کامل"
|
||||
"batchFileDownloadFull": "دانلود فایل کامل",
|
||||
"batchHeaderSubtitle": "Run many requests as one job",
|
||||
"batchStep1": "1 · Upload JSONL",
|
||||
"batchStep1Desc": "Add requests",
|
||||
"batchStep2": "2 · Create batch",
|
||||
"batchStep2Desc": "Run job",
|
||||
"batchStep3": "3 · Get results",
|
||||
"batchStep3Desc": "Download output"
|
||||
},
|
||||
"disabled": "Disabled",
|
||||
"featureFlagOmnirouteEmergencyFallbackDescription": "Route budget-exhausted requests to the emergency free fallback provider/model.",
|
||||
@@ -1293,7 +1300,8 @@
|
||||
"open": "باز کردن",
|
||||
"close": "بستن"
|
||||
},
|
||||
"noResults": "هیچ نتیجهای یافت نشد"
|
||||
"noResults": "هیچ نتیجهای یافت نشد",
|
||||
"trafficInspectorPurpose": "See exactly what your application sends to and receives from AI providers. Works with any OpenAI-compatible client."
|
||||
},
|
||||
"webhooks": {
|
||||
"title": "وب هوک ها",
|
||||
@@ -1856,7 +1864,21 @@
|
||||
"directDownloadHint": "یا فرمت نصبکننده مربوطه را بهطور مستقیم دانلود کنید:",
|
||||
"releaseNotes": "یادداشتهای انتشار",
|
||||
"readMore": "بیشتر بخوانید",
|
||||
"noAuthLabel": "بدون احراز هویت"
|
||||
"noAuthLabel": "بدون احراز هویت",
|
||||
"recentRequests": "Recent Requests",
|
||||
"recentRequestsEmpty": "No requests yet.",
|
||||
"recentRequestsModel": "Model",
|
||||
"recentRequestsTokens": "In / Out",
|
||||
"recentRequestsWhen": "When",
|
||||
"readinessEyebrow": "Get ready to route",
|
||||
"readinessTitle": "Send your first request",
|
||||
"readinessSubtitle": "Four small steps. OmniRoute checks readiness as you go.",
|
||||
"readinessStep1": "Connect a provider",
|
||||
"readinessStep2": "Configure endpoint authentication",
|
||||
"readinessStep3": "Copy your endpoint",
|
||||
"readinessStep4": "Send a test request",
|
||||
"readinessContinue": "Continue setup",
|
||||
"readinessDismiss": "Dismiss for now"
|
||||
},
|
||||
"analytics": {
|
||||
"title": "Analytics",
|
||||
@@ -2901,7 +2923,8 @@
|
||||
"omp": "عامل کدنویسی ترمینال Oh My Pi",
|
||||
"letta": "عامل Letta CLI با حافظه پایدار و استفاده از ابزار",
|
||||
"warp": "ترمینال هوش مصنوعی Warp با پشتیبانی از ارائهدهنده سفارشی",
|
||||
"agent-deck": "ارکستراتور چندعاملی Agent Deck"
|
||||
"agent-deck": "ارکستراتور چندعاملی Agent Deck",
|
||||
"prime-agent": "Prime Agent — self-improving RLM coding harness with OpenAI-compatible provider support"
|
||||
},
|
||||
"guides": {
|
||||
"cursor": {
|
||||
@@ -4133,7 +4156,10 @@
|
||||
"notionIntegrationHelp": "یک ادغام داخلی در",
|
||||
"notionIntegrationToken": "توکن ادغام داخلی نوتیون",
|
||||
"notionNotConnected": "متصل نیستید",
|
||||
"notionTokenConfigured": "توکن پیکربندی شده است. ابزارهای Notion از طریق MCP در دسترس هستند."
|
||||
"notionTokenConfigured": "توکن پیکربندی شده است. ابزارهای Notion از طریق MCP در دسترس هستند.",
|
||||
"subtitle": "Use the OpenAI-compatible endpoint with most SDKs and tools.",
|
||||
"testEndpoint": "Test endpoint →",
|
||||
"advancedProtocols": "Advanced protocols"
|
||||
},
|
||||
"endpoints": {
|
||||
"tabProxy": "Endpoint Proxy",
|
||||
@@ -4716,7 +4742,14 @@
|
||||
"issueCount": "{count} مشکل",
|
||||
"score": "امتیاز",
|
||||
"lastRequest": "آخرین درخواست",
|
||||
"lastError": "آخرین خطا"
|
||||
"lastError": "آخرین خطا",
|
||||
"healthVerdictReady": "OmniRoute is ready",
|
||||
"healthVerdictActionRequired": "Action required to restore full operation",
|
||||
"healthVerdictCoolingDown": "Cooling down after recent changes",
|
||||
"healthSubtitle": "System health check",
|
||||
"advancedDiagnosticsTitle": "Advanced diagnostics",
|
||||
"hide": "Hide",
|
||||
"show": "Show"
|
||||
},
|
||||
"telemetry": {
|
||||
"title": "سیستم تله متری",
|
||||
@@ -6383,7 +6416,21 @@
|
||||
"savedModelEndpointSettings": "تنظیمات نقطه پایانی مدل ذخیره شده",
|
||||
"searchByModelAria": "جستجو بر اساس مدل",
|
||||
"selectSupportedEndpoint": "حداقل یک نقطه پایانی پشتیبانی شده را انتخاب کنید",
|
||||
"antigravityClientProfileHarness": "Harness / CLI"
|
||||
"antigravityClientProfileHarness": "Harness / CLI",
|
||||
"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."
|
||||
},
|
||||
"settings": {
|
||||
"title": "Settings",
|
||||
@@ -8230,7 +8277,19 @@
|
||||
"cliproxyapiHealth": "سلامت",
|
||||
"cliproxyapiPort": "پورت",
|
||||
"qdrantHost": "میزبان",
|
||||
"qdrantCollection": "مجموعه"
|
||||
"qdrantCollection": "مجموعه",
|
||||
"presetAll": "All",
|
||||
"presetAllDesc": "Show everything",
|
||||
"presetEssentials": "Essentials",
|
||||
"presetEssentialsDesc": "Beginner path - Advanced tools stay searchable",
|
||||
"presetMinimal": "Minimal",
|
||||
"presetMinimalDesc": "Core pages only",
|
||||
"presetDeveloper": "Developer",
|
||||
"presetDeveloperDesc": "Dev & proxy tools",
|
||||
"presetAdmin": "Admin",
|
||||
"presetAdminDesc": "Monitoring & audit",
|
||||
"settingsSidebarTitle": "Sidebar Customization",
|
||||
"settingsSidebarDesc": "Choose which sidebar items to show. Essentials keeps Advanced tools searchable."
|
||||
},
|
||||
"contextRtk": {
|
||||
"title": "RTK Engine",
|
||||
@@ -12017,7 +12076,8 @@
|
||||
"title": "عاملهای ACP",
|
||||
"phrase": "CLIهایی که OmniRoute به عنوان بکاند اجرا ایجاد میکند (جریان معکوس)",
|
||||
"flow": "کلاینت → OmniRoute → ایجاد CLI (stdio/ACP) → پاسخ",
|
||||
"seeOther": "مشاهده →"
|
||||
"seeOther": "مشاهده →",
|
||||
"warning": "Most users can ignore this — use only when an integration requires it."
|
||||
}
|
||||
},
|
||||
"comparison": {
|
||||
@@ -12841,6 +12901,10 @@
|
||||
},
|
||||
"SKILLS_SANDBOX_NETWORK_ENABLED": {
|
||||
"description": "فعالسازی دسترسی به شبکه در محیط ایزوله مهارتها."
|
||||
},
|
||||
"DISABLE_CONTEXT_WINDOW_CHECKS": {
|
||||
"label": "Disable Context Window Checks",
|
||||
"description": "Skip OmniRoute's local context-window and max-input-token check for direct single-model requests. Upstream providers still enforce their actual limits. Prompt compression and output-token caps remain active."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -13413,6 +13477,13 @@
|
||||
"modelLockouts": "قفلهای مدل",
|
||||
"count": "تعداد اتصالات"
|
||||
}
|
||||
},
|
||||
"reassuranceTitle": "Your connections recover automatically",
|
||||
"reassuranceDetail": "Usually no action is needed. OmniRoute temporarily rests a connection after failures, then safely tries it again.",
|
||||
"plainStates": {
|
||||
"healthy": "Requests can be sent",
|
||||
"coolingDown": "Trying again soon",
|
||||
"lockedOut": "Needs your attention"
|
||||
}
|
||||
},
|
||||
"featureFlagCapabilityFilterEnabledDescription": "درخواستها را قبل از ارسال رد کنید زمانی که مدل هدف قابلیتهای مورد نیاز (بینایی، ابزارها، خروجی ساختاریافته، پنجره زمینه) را ندارد. از درخواستهای مستقیم تکتأمینکننده که فیلتر سازگاری لایه ترکیبی را دور میزنند، محافظت میکند.",
|
||||
@@ -13850,5 +13921,13 @@
|
||||
"toolsMismatch": "ارائهدهنده از فراخوانی ابزار پشتیبانی نمیکند",
|
||||
"structuredOutputMismatch": "ارائهدهنده خروجی ساختاریافته را پشتیبانی نمیکند",
|
||||
"contextWindowMismatch": "درخواست از حد مجاز پنجره زمینه ارائهدهنده فراتر میرود"
|
||||
},
|
||||
"featureFlagDisableContextWindowChecksDescription": "Skip OmniRoute's local context-window and max-input-token check for direct single-model requests. Upstream providers still enforce their actual limits. Prompt compression and output-token caps remain active.",
|
||||
"cheaperInferenceSponsorBanner": {
|
||||
"title": "Cheaper Inference is an OmniRoute Open Source Friend",
|
||||
"description": "A cost-ranked gateway reselling dozens of frontier models behind one OpenAI-compatible endpoint — routing each request to the cheapest eligible provider, never above list price.",
|
||||
"cta": "Get an API Key",
|
||||
"partnerLinkNote": "Partner link",
|
||||
"dismissAriaLabel": "Dismiss"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -974,7 +974,14 @@
|
||||
"batchFileUsedByCount": "Käytetään {count, plural, one {# erä} other {# erää}}",
|
||||
"batchFilePreview": "Esikatselu",
|
||||
"batchFilePreviewTruncated": "Näytetään ensimmäiset {shown} riviä ({total} yhteensä riviä)",
|
||||
"batchFileDownloadFull": "Lataa Koko Tiedosto"
|
||||
"batchFileDownloadFull": "Lataa Koko Tiedosto",
|
||||
"batchHeaderSubtitle": "Run many requests as one job",
|
||||
"batchStep1": "1 · Upload JSONL",
|
||||
"batchStep1Desc": "Add requests",
|
||||
"batchStep2": "2 · Create batch",
|
||||
"batchStep2Desc": "Run job",
|
||||
"batchStep3": "3 · Get results",
|
||||
"batchStep3Desc": "Download output"
|
||||
},
|
||||
"disabled": "Poistettu käytöstä",
|
||||
"featureFlagOmnirouteEmergencyFallbackDescription": "Reititä budjettinsa ylittäneet pyynnöt varalla olevalle ilmaiselle varatarjoajalle/-mallille.",
|
||||
@@ -1293,7 +1300,8 @@
|
||||
"open": "avaa",
|
||||
"close": "sulje"
|
||||
},
|
||||
"noResults": "Ei tuloksia"
|
||||
"noResults": "Ei tuloksia",
|
||||
"trafficInspectorPurpose": "See exactly what your application sends to and receives from AI providers. Works with any OpenAI-compatible client."
|
||||
},
|
||||
"webhooks": {
|
||||
"title": "Webhooks",
|
||||
@@ -1856,7 +1864,21 @@
|
||||
"directDownloadHint": "Tai lataa vastaava asennustiedosto suoraan:",
|
||||
"releaseNotes": "Julkaisutiedot",
|
||||
"readMore": "Lue lisää",
|
||||
"noAuthLabel": "Ei todennusta"
|
||||
"noAuthLabel": "Ei todennusta",
|
||||
"recentRequests": "Recent Requests",
|
||||
"recentRequestsEmpty": "No requests yet.",
|
||||
"recentRequestsModel": "Model",
|
||||
"recentRequestsTokens": "In / Out",
|
||||
"recentRequestsWhen": "When",
|
||||
"readinessEyebrow": "Get ready to route",
|
||||
"readinessTitle": "Send your first request",
|
||||
"readinessSubtitle": "Four small steps. OmniRoute checks readiness as you go.",
|
||||
"readinessStep1": "Connect a provider",
|
||||
"readinessStep2": "Configure endpoint authentication",
|
||||
"readinessStep3": "Copy your endpoint",
|
||||
"readinessStep4": "Send a test request",
|
||||
"readinessContinue": "Continue setup",
|
||||
"readinessDismiss": "Dismiss for now"
|
||||
},
|
||||
"analytics": {
|
||||
"title": "Analytics",
|
||||
@@ -2901,7 +2923,8 @@
|
||||
"omp": "Oh My Pi -terminaalikoodausagentti",
|
||||
"letta": "Letta CLI -agentti pysyvällä muistilla ja työkalujen käytöllä",
|
||||
"warp": "Warp AI -terminaali mukautetun palveluntarjoajan tuella",
|
||||
"agent-deck": "Agent Deck -moniagenttiorkestraattori"
|
||||
"agent-deck": "Agent Deck -moniagenttiorkestraattori",
|
||||
"prime-agent": "Prime Agent — self-improving RLM coding harness with OpenAI-compatible provider support"
|
||||
},
|
||||
"guides": {
|
||||
"cursor": {
|
||||
@@ -4133,7 +4156,10 @@
|
||||
"notionIntegrationHelp": "Luo sisäinen integraatio kohdassa",
|
||||
"notionIntegrationToken": "Notionin sisäinen integraatiotunnus",
|
||||
"notionNotConnected": "Ei yhdistetty",
|
||||
"notionTokenConfigured": "Token on määritetty. Notion-työkalut ovat saatavilla MCP:n kautta."
|
||||
"notionTokenConfigured": "Token on määritetty. Notion-työkalut ovat saatavilla MCP:n kautta.",
|
||||
"subtitle": "Use the OpenAI-compatible endpoint with most SDKs and tools.",
|
||||
"testEndpoint": "Test endpoint →",
|
||||
"advancedProtocols": "Advanced protocols"
|
||||
},
|
||||
"endpoints": {
|
||||
"tabProxy": "Endpoint Proxy",
|
||||
@@ -4716,7 +4742,14 @@
|
||||
"issueCount": "{count} ongelmaa",
|
||||
"score": "Pisteet",
|
||||
"lastRequest": "Viimeisin pyyntö",
|
||||
"lastError": "Viimeisin virhe"
|
||||
"lastError": "Viimeisin virhe",
|
||||
"healthVerdictReady": "OmniRoute is ready",
|
||||
"healthVerdictActionRequired": "Action required to restore full operation",
|
||||
"healthVerdictCoolingDown": "Cooling down after recent changes",
|
||||
"healthSubtitle": "System health check",
|
||||
"advancedDiagnosticsTitle": "Advanced diagnostics",
|
||||
"hide": "Hide",
|
||||
"show": "Show"
|
||||
},
|
||||
"telemetry": {
|
||||
"title": "Järjestelmän telemetria",
|
||||
@@ -6383,7 +6416,21 @@
|
||||
"savedModelEndpointSettings": "Tallennetun mallin päätepisteen asetukset",
|
||||
"searchByModelAria": "Hae mallin mukaan",
|
||||
"selectSupportedEndpoint": "Valitse vähintään yksi tuettu päätepiste",
|
||||
"antigravityClientProfileHarness": "Harness / CLI"
|
||||
"antigravityClientProfileHarness": "Harness / CLI",
|
||||
"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."
|
||||
},
|
||||
"settings": {
|
||||
"title": "Asetukset",
|
||||
@@ -8230,7 +8277,19 @@
|
||||
"cliproxyapiHealth": "Terveys",
|
||||
"cliproxyapiPort": "Portti",
|
||||
"qdrantHost": "Isäntä",
|
||||
"qdrantCollection": "Kokoelma"
|
||||
"qdrantCollection": "Kokoelma",
|
||||
"presetAll": "All",
|
||||
"presetAllDesc": "Show everything",
|
||||
"presetEssentials": "Essentials",
|
||||
"presetEssentialsDesc": "Beginner path - Advanced tools stay searchable",
|
||||
"presetMinimal": "Minimal",
|
||||
"presetMinimalDesc": "Core pages only",
|
||||
"presetDeveloper": "Developer",
|
||||
"presetDeveloperDesc": "Dev & proxy tools",
|
||||
"presetAdmin": "Admin",
|
||||
"presetAdminDesc": "Monitoring & audit",
|
||||
"settingsSidebarTitle": "Sidebar Customization",
|
||||
"settingsSidebarDesc": "Choose which sidebar items to show. Essentials keeps Advanced tools searchable."
|
||||
},
|
||||
"contextRtk": {
|
||||
"title": "RTK Engine",
|
||||
@@ -12017,7 +12076,8 @@
|
||||
"title": "ACP-agentit",
|
||||
"phrase": "CLI:t, jotka OmniRoute käynnistää suoritustaustana (käänteinen virtaus)",
|
||||
"flow": "Asiakas → OmniRoute → käynnistä CLI (stdio/ACP) → vastaus",
|
||||
"seeOther": "Katso →"
|
||||
"seeOther": "Katso →",
|
||||
"warning": "Most users can ignore this — use only when an integration requires it."
|
||||
}
|
||||
},
|
||||
"comparison": {
|
||||
@@ -12841,6 +12901,10 @@
|
||||
},
|
||||
"SKILLS_SANDBOX_NETWORK_ENABLED": {
|
||||
"description": "Ota käyttöön verkkoyhteys taitojen hiekkalaatikossa."
|
||||
},
|
||||
"DISABLE_CONTEXT_WINDOW_CHECKS": {
|
||||
"label": "Disable Context Window Checks",
|
||||
"description": "Skip OmniRoute's local context-window and max-input-token check for direct single-model requests. Upstream providers still enforce their actual limits. Prompt compression and output-token caps remain active."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -13413,6 +13477,13 @@
|
||||
"modelLockouts": "Mallilukitukset",
|
||||
"count": "Yhteyksien Määrä"
|
||||
}
|
||||
},
|
||||
"reassuranceTitle": "Your connections recover automatically",
|
||||
"reassuranceDetail": "Usually no action is needed. OmniRoute temporarily rests a connection after failures, then safely tries it again.",
|
||||
"plainStates": {
|
||||
"healthy": "Requests can be sent",
|
||||
"coolingDown": "Trying again soon",
|
||||
"lockedOut": "Needs your attention"
|
||||
}
|
||||
},
|
||||
"featureFlagCapabilityFilterEnabledDescription": "Hylkää pyynnöt ennen lähettämistä, kun kohdemallilta puuttuu vaadittuja ominaisuuksia (näkö, työkalut, jäsennelty ulostulo, kontekstikkelu). Suojaa suorat yhden tarjoajan pyynnöt, jotka ohittavat yhdistelmäkerroksen yhteensopivuussuodattimen.",
|
||||
@@ -13850,5 +13921,13 @@
|
||||
"toolsMismatch": "Toimittaja ei tue työkalun kutsumista",
|
||||
"structuredOutputMismatch": "Palveluntarjoaja ei tue jäsenneltyä tulostusta",
|
||||
"contextWindowMismatch": "Pyyntö ylittää tarjoajan kontekstin ikkunan"
|
||||
},
|
||||
"featureFlagDisableContextWindowChecksDescription": "Skip OmniRoute's local context-window and max-input-token check for direct single-model requests. Upstream providers still enforce their actual limits. Prompt compression and output-token caps remain active.",
|
||||
"cheaperInferenceSponsorBanner": {
|
||||
"title": "Cheaper Inference is an OmniRoute Open Source Friend",
|
||||
"description": "A cost-ranked gateway reselling dozens of frontier models behind one OpenAI-compatible endpoint — routing each request to the cheapest eligible provider, never above list price.",
|
||||
"cta": "Get an API Key",
|
||||
"partnerLinkNote": "Partner link",
|
||||
"dismissAriaLabel": "Dismiss"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -974,7 +974,14 @@
|
||||
"batchFileUsedByCount": "Utilisé par {count, plural, one {# lot} other {# lots}}",
|
||||
"batchFilePreview": "Aperçu",
|
||||
"batchFilePreviewTruncated": "Affichage des {shown} premières lignes ({total} lignes au total)",
|
||||
"batchFileDownloadFull": "Télécharger le fichier complet"
|
||||
"batchFileDownloadFull": "Télécharger le fichier complet",
|
||||
"batchHeaderSubtitle": "Run many requests as one job",
|
||||
"batchStep1": "1 · Upload JSONL",
|
||||
"batchStep1Desc": "Add requests",
|
||||
"batchStep2": "2 · Create batch",
|
||||
"batchStep2Desc": "Run job",
|
||||
"batchStep3": "3 · Get results",
|
||||
"batchStep3Desc": "Download output"
|
||||
},
|
||||
"disabled": "Désactivé",
|
||||
"featureFlagOmnirouteEmergencyFallbackDescription": "Router les requêtes ayant épuisé leur budget vers le fournisseur/modèle de secours gratuit d'urgence.",
|
||||
@@ -1293,7 +1300,8 @@
|
||||
"open": "ouvrir",
|
||||
"close": "fermer"
|
||||
},
|
||||
"noResults": "Aucun résultat"
|
||||
"noResults": "Aucun résultat",
|
||||
"trafficInspectorPurpose": "See exactly what your application sends to and receives from AI providers. Works with any OpenAI-compatible client."
|
||||
},
|
||||
"webhooks": {
|
||||
"title": "Webhooks",
|
||||
@@ -1856,7 +1864,21 @@
|
||||
"directDownloadHint": "Vous pouvez aussi télécharger directement le format d'installation adapté :",
|
||||
"releaseNotes": "Notes de version",
|
||||
"readMore": "Lire la suite",
|
||||
"noAuthLabel": "Sans authentification"
|
||||
"noAuthLabel": "Sans authentification",
|
||||
"recentRequests": "Recent Requests",
|
||||
"recentRequestsEmpty": "No requests yet.",
|
||||
"recentRequestsModel": "Model",
|
||||
"recentRequestsTokens": "In / Out",
|
||||
"recentRequestsWhen": "When",
|
||||
"readinessEyebrow": "Get ready to route",
|
||||
"readinessTitle": "Send your first request",
|
||||
"readinessSubtitle": "Four small steps. OmniRoute checks readiness as you go.",
|
||||
"readinessStep1": "Connect a provider",
|
||||
"readinessStep2": "Configure endpoint authentication",
|
||||
"readinessStep3": "Copy your endpoint",
|
||||
"readinessStep4": "Send a test request",
|
||||
"readinessContinue": "Continue setup",
|
||||
"readinessDismiss": "Dismiss for now"
|
||||
},
|
||||
"analytics": {
|
||||
"title": "Analyse",
|
||||
@@ -2901,7 +2923,8 @@
|
||||
"omp": "Agent de codage de terminal Oh My Pi",
|
||||
"letta": "Agent CLI Letta avec mémoire persistante et utilisation d'outils",
|
||||
"warp": "Terminal Warp AI avec prise en charge de fournisseur personnalisé",
|
||||
"agent-deck": "Orchestrateur multi-agent Agent Deck"
|
||||
"agent-deck": "Orchestrateur multi-agent Agent Deck",
|
||||
"prime-agent": "Prime Agent — self-improving RLM coding harness with OpenAI-compatible provider support"
|
||||
},
|
||||
"guides": {
|
||||
"cursor": {
|
||||
@@ -4133,7 +4156,10 @@
|
||||
"notionIntegrationHelp": "Créer an Internal Integration at",
|
||||
"notionIntegrationToken": "Jeton d’intégration interne Notion",
|
||||
"notionNotConnected": "Non connecté",
|
||||
"notionTokenConfigured": "Jeton configuré. Les outils Notion sont disponibles via MCP."
|
||||
"notionTokenConfigured": "Jeton configuré. Les outils Notion sont disponibles via MCP.",
|
||||
"subtitle": "Use the OpenAI-compatible endpoint with most SDKs and tools.",
|
||||
"testEndpoint": "Test endpoint →",
|
||||
"advancedProtocols": "Advanced protocols"
|
||||
},
|
||||
"endpoints": {
|
||||
"tabProxy": "Endpoint Proxy",
|
||||
@@ -4716,7 +4742,14 @@
|
||||
"issueCount": "{count} problèmes",
|
||||
"score": "Score",
|
||||
"lastRequest": "Dernière requête",
|
||||
"lastError": "Dernière erreur"
|
||||
"lastError": "Dernière erreur",
|
||||
"healthVerdictReady": "OmniRoute is ready",
|
||||
"healthVerdictActionRequired": "Action required to restore full operation",
|
||||
"healthVerdictCoolingDown": "Cooling down after recent changes",
|
||||
"healthSubtitle": "System health check",
|
||||
"advancedDiagnosticsTitle": "Advanced diagnostics",
|
||||
"hide": "Hide",
|
||||
"show": "Show"
|
||||
},
|
||||
"telemetry": {
|
||||
"title": "Télémétrie du système",
|
||||
@@ -6383,7 +6416,21 @@
|
||||
"savedModelEndpointSettings": "Saved modèles endpoint paramètres",
|
||||
"searchByModelAria": "Rechercher un modèle",
|
||||
"selectSupportedEndpoint": "Sélectionnez au moins un endpoint pris en charge",
|
||||
"antigravityClientProfileHarness": "Harness / CLI"
|
||||
"antigravityClientProfileHarness": "Harness / CLI",
|
||||
"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."
|
||||
},
|
||||
"settings": {
|
||||
"title": "Paramètres",
|
||||
@@ -8230,7 +8277,19 @@
|
||||
"cliproxyapiHealth": "Santé",
|
||||
"cliproxyapiPort": "Port",
|
||||
"qdrantHost": "Hôte",
|
||||
"qdrantCollection": "Collection"
|
||||
"qdrantCollection": "Collection",
|
||||
"presetAll": "All",
|
||||
"presetAllDesc": "Show everything",
|
||||
"presetEssentials": "Essentials",
|
||||
"presetEssentialsDesc": "Beginner path - Advanced tools stay searchable",
|
||||
"presetMinimal": "Minimal",
|
||||
"presetMinimalDesc": "Core pages only",
|
||||
"presetDeveloper": "Developer",
|
||||
"presetDeveloperDesc": "Dev & proxy tools",
|
||||
"presetAdmin": "Admin",
|
||||
"presetAdminDesc": "Monitoring & audit",
|
||||
"settingsSidebarTitle": "Sidebar Customization",
|
||||
"settingsSidebarDesc": "Choose which sidebar items to show. Essentials keeps Advanced tools searchable."
|
||||
},
|
||||
"contextRtk": {
|
||||
"title": "RTK Engine",
|
||||
@@ -12017,7 +12076,8 @@
|
||||
"title": "Agents ACP",
|
||||
"phrase": "CLI qu'OmniRoute lance en tant que backend d'exécution (flux inverse)",
|
||||
"flow": "Client → OmniRoute → spawn CLI (stdio/ACP) → réponse",
|
||||
"seeOther": "Voir →"
|
||||
"seeOther": "Voir →",
|
||||
"warning": "Most users can ignore this — use only when an integration requires it."
|
||||
}
|
||||
},
|
||||
"comparison": {
|
||||
@@ -12841,6 +12901,10 @@
|
||||
},
|
||||
"SKILLS_SANDBOX_NETWORK_ENABLED": {
|
||||
"description": "Activer l'accès réseau dans le bac à sable des compétences."
|
||||
},
|
||||
"DISABLE_CONTEXT_WINDOW_CHECKS": {
|
||||
"label": "Disable Context Window Checks",
|
||||
"description": "Skip OmniRoute's local context-window and max-input-token check for direct single-model requests. Upstream providers still enforce their actual limits. Prompt compression and output-token caps remain active."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -13413,6 +13477,13 @@
|
||||
"modelLockouts": "Blocages de Modèle",
|
||||
"count": "Nombre de Connexions"
|
||||
}
|
||||
},
|
||||
"reassuranceTitle": "Your connections recover automatically",
|
||||
"reassuranceDetail": "Usually no action is needed. OmniRoute temporarily rests a connection after failures, then safely tries it again.",
|
||||
"plainStates": {
|
||||
"healthy": "Requests can be sent",
|
||||
"coolingDown": "Trying again soon",
|
||||
"lockedOut": "Needs your attention"
|
||||
}
|
||||
},
|
||||
"featureFlagCapabilityFilterEnabledDescription": "Rejeter les demandes avant l'expédition lorsque le modèle cible manque des capacités requises (vision, outils, sortie structurée, fenêtre de contexte). Protège les demandes directes à un seul fournisseur qui contournent le filtre de compatibilité de la couche combo.",
|
||||
@@ -13850,5 +13921,13 @@
|
||||
"toolsMismatch": "Le fournisseur ne prend pas en charge l'appel d'outils",
|
||||
"structuredOutputMismatch": "Le fournisseur ne prend pas en charge la sortie structurée",
|
||||
"contextWindowMismatch": "La demande dépasse la fenêtre de contexte du fournisseur"
|
||||
},
|
||||
"featureFlagDisableContextWindowChecksDescription": "Skip OmniRoute's local context-window and max-input-token check for direct single-model requests. Upstream providers still enforce their actual limits. Prompt compression and output-token caps remain active.",
|
||||
"cheaperInferenceSponsorBanner": {
|
||||
"title": "Cheaper Inference is an OmniRoute Open Source Friend",
|
||||
"description": "A cost-ranked gateway reselling dozens of frontier models behind one OpenAI-compatible endpoint — routing each request to the cheapest eligible provider, never above list price.",
|
||||
"cta": "Get an API Key",
|
||||
"partnerLinkNote": "Partner link",
|
||||
"dismissAriaLabel": "Dismiss"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -974,7 +974,14 @@
|
||||
"batchFileUsedByCount": "{count, plural, one {# બેચ} other {# બેચો}}",
|
||||
"batchFilePreview": "પૂર્વદર્શન",
|
||||
"batchFilePreviewTruncated": "પ્રથમ {shown} લાઈનો દર્શાવી રહ્યા છીએ ({total} કુલ લાઈનો)",
|
||||
"batchFileDownloadFull": "પૂર્ણ ફાઇલ ડાઉનલોડ કરો"
|
||||
"batchFileDownloadFull": "પૂર્ણ ફાઇલ ડાઉનલોડ કરો",
|
||||
"batchHeaderSubtitle": "Run many requests as one job",
|
||||
"batchStep1": "1 · Upload JSONL",
|
||||
"batchStep1Desc": "Add requests",
|
||||
"batchStep2": "2 · Create batch",
|
||||
"batchStep2Desc": "Run job",
|
||||
"batchStep3": "3 · Get results",
|
||||
"batchStep3Desc": "Download output"
|
||||
},
|
||||
"disabled": "નિષ્ક્રિય કરેલ",
|
||||
"featureFlagOmnirouteEmergencyFallbackDescription": "બજેટ-સમાપ્ત વિનંતીઓને કટોકટીના મફત ફોલબેક પ્રદાતા/મોડેલ પર રૂટ કરો.",
|
||||
@@ -1293,7 +1300,8 @@
|
||||
"open": "ખોલો",
|
||||
"close": "બંધ કરો"
|
||||
},
|
||||
"noResults": "કોઈ પરિણામો નથી"
|
||||
"noResults": "કોઈ પરિણામો નથી",
|
||||
"trafficInspectorPurpose": "See exactly what your application sends to and receives from AI providers. Works with any OpenAI-compatible client."
|
||||
},
|
||||
"webhooks": {
|
||||
"title": "વેબહુક્સ",
|
||||
@@ -1856,7 +1864,21 @@
|
||||
"directDownloadHint": "અથવા સંબંધિત ઇન્સ્ટોલર ફોર્મેટ સીધા ડાઉનલોડ કરો:",
|
||||
"releaseNotes": "રિલીઝ નોંધો",
|
||||
"readMore": "વધુ વાંચો",
|
||||
"noAuthLabel": "કોઈ ઓથેન્ટિકેશન નથી"
|
||||
"noAuthLabel": "કોઈ ઓથેન્ટિકેશન નથી",
|
||||
"recentRequests": "Recent Requests",
|
||||
"recentRequestsEmpty": "No requests yet.",
|
||||
"recentRequestsModel": "Model",
|
||||
"recentRequestsTokens": "In / Out",
|
||||
"recentRequestsWhen": "When",
|
||||
"readinessEyebrow": "Get ready to route",
|
||||
"readinessTitle": "Send your first request",
|
||||
"readinessSubtitle": "Four small steps. OmniRoute checks readiness as you go.",
|
||||
"readinessStep1": "Connect a provider",
|
||||
"readinessStep2": "Configure endpoint authentication",
|
||||
"readinessStep3": "Copy your endpoint",
|
||||
"readinessStep4": "Send a test request",
|
||||
"readinessContinue": "Continue setup",
|
||||
"readinessDismiss": "Dismiss for now"
|
||||
},
|
||||
"analytics": {
|
||||
"title": "Analytics",
|
||||
@@ -2901,7 +2923,8 @@
|
||||
"omp": "Oh My Pi ટર્મિનલ કોડિંગ એજન્ટ",
|
||||
"letta": "પર્સિસ્ટન્ટ મેમરી અને ટૂલ વપરાશ સાથે Letta CLI એજન્ટ",
|
||||
"warp": "કસ્ટમ પ્રોવાઇડર સપોર્ટ સાથે Warp AI ટર્મિનલ",
|
||||
"agent-deck": "Agent Deck મલ્ટિ-એજન્ટ ઓર્કેસ્ટ્રેટર"
|
||||
"agent-deck": "Agent Deck મલ્ટિ-એજન્ટ ઓર્કેસ્ટ્રેટર",
|
||||
"prime-agent": "Prime Agent — self-improving RLM coding harness with OpenAI-compatible provider support"
|
||||
},
|
||||
"guides": {
|
||||
"cursor": {
|
||||
@@ -4133,7 +4156,10 @@
|
||||
"notionIntegrationHelp": "આંતરિક એકીકરણ બનાવો પર",
|
||||
"notionIntegrationToken": "Notion આંતરિક ઇન્ટિગ્રેશન ટોકન",
|
||||
"notionNotConnected": "જોડાયેલ નથી",
|
||||
"notionTokenConfigured": "ટોકન કન્ફિગર કરાયું છે. Notion ટૂલ્સ MCP દ્વારા ઉપલબ્ધ છે."
|
||||
"notionTokenConfigured": "ટોકન કન્ફિગર કરાયું છે. Notion ટૂલ્સ MCP દ્વારા ઉપલબ્ધ છે.",
|
||||
"subtitle": "Use the OpenAI-compatible endpoint with most SDKs and tools.",
|
||||
"testEndpoint": "Test endpoint →",
|
||||
"advancedProtocols": "Advanced protocols"
|
||||
},
|
||||
"endpoints": {
|
||||
"tabProxy": "Endpoint Proxy",
|
||||
@@ -4716,7 +4742,14 @@
|
||||
"issueCount": "{count} સમસ્યાઓ",
|
||||
"score": "સ્કોર",
|
||||
"lastRequest": "છેલ્લી વિનંતી",
|
||||
"lastError": "છેલ્લી ભૂલ"
|
||||
"lastError": "છેલ્લી ભૂલ",
|
||||
"healthVerdictReady": "OmniRoute is ready",
|
||||
"healthVerdictActionRequired": "Action required to restore full operation",
|
||||
"healthVerdictCoolingDown": "Cooling down after recent changes",
|
||||
"healthSubtitle": "System health check",
|
||||
"advancedDiagnosticsTitle": "Advanced diagnostics",
|
||||
"hide": "Hide",
|
||||
"show": "Show"
|
||||
},
|
||||
"telemetry": {
|
||||
"title": "સિસ્ટમ ટેલિમેટ્રી",
|
||||
@@ -6383,7 +6416,21 @@
|
||||
"savedModelEndpointSettings": "સાચવેલ મોડેલ અંતિમ બિંદુની સેટિંગ્સ",
|
||||
"searchByModelAria": "મોડલ દ્વારા શોધો",
|
||||
"selectSupportedEndpoint": "કમથી કમ એક સમર્થિત અંતિમ બિંદુ પસંદ કરો",
|
||||
"antigravityClientProfileHarness": "Harness / CLI"
|
||||
"antigravityClientProfileHarness": "Harness / CLI",
|
||||
"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."
|
||||
},
|
||||
"settings": {
|
||||
"title": "Settings",
|
||||
@@ -8230,7 +8277,19 @@
|
||||
"cliproxyapiHealth": "આરોગ્ય",
|
||||
"cliproxyapiPort": "પોર્ટ",
|
||||
"qdrantHost": "હોસ્ટ",
|
||||
"qdrantCollection": "સંગ્રહ"
|
||||
"qdrantCollection": "સંગ્રહ",
|
||||
"presetAll": "All",
|
||||
"presetAllDesc": "Show everything",
|
||||
"presetEssentials": "Essentials",
|
||||
"presetEssentialsDesc": "Beginner path - Advanced tools stay searchable",
|
||||
"presetMinimal": "Minimal",
|
||||
"presetMinimalDesc": "Core pages only",
|
||||
"presetDeveloper": "Developer",
|
||||
"presetDeveloperDesc": "Dev & proxy tools",
|
||||
"presetAdmin": "Admin",
|
||||
"presetAdminDesc": "Monitoring & audit",
|
||||
"settingsSidebarTitle": "Sidebar Customization",
|
||||
"settingsSidebarDesc": "Choose which sidebar items to show. Essentials keeps Advanced tools searchable."
|
||||
},
|
||||
"contextRtk": {
|
||||
"title": "RTK Engine",
|
||||
@@ -12017,7 +12076,8 @@
|
||||
"title": "ACP એજન્ટ્સ",
|
||||
"phrase": "CLIs જે OmniRoute એક્ઝિક્યુશન બેકએન્ડ તરીકે શરૂ કરે છે (રિવર્સ ફ્લો)",
|
||||
"flow": "ક્લાયન્ટ → OmniRoute → spawn CLI (stdio/ACP) → પ્રતિસાદ",
|
||||
"seeOther": "જુઓ →"
|
||||
"seeOther": "જુઓ →",
|
||||
"warning": "Most users can ignore this — use only when an integration requires it."
|
||||
}
|
||||
},
|
||||
"comparison": {
|
||||
@@ -12841,6 +12901,10 @@
|
||||
},
|
||||
"SKILLS_SANDBOX_NETWORK_ENABLED": {
|
||||
"description": "સ્કિલ્સ સેન્ડબોક્સમાં નેટવર્ક એક્સેસ સક્ષમ કરો."
|
||||
},
|
||||
"DISABLE_CONTEXT_WINDOW_CHECKS": {
|
||||
"label": "Disable Context Window Checks",
|
||||
"description": "Skip OmniRoute's local context-window and max-input-token check for direct single-model requests. Upstream providers still enforce their actual limits. Prompt compression and output-token caps remain active."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -13413,6 +13477,13 @@
|
||||
"modelLockouts": "મોડેલ લોકઆઉટ",
|
||||
"count": "કનેક્શન ગણતરી"
|
||||
}
|
||||
},
|
||||
"reassuranceTitle": "Your connections recover automatically",
|
||||
"reassuranceDetail": "Usually no action is needed. OmniRoute temporarily rests a connection after failures, then safely tries it again.",
|
||||
"plainStates": {
|
||||
"healthy": "Requests can be sent",
|
||||
"coolingDown": "Trying again soon",
|
||||
"lockedOut": "Needs your attention"
|
||||
}
|
||||
},
|
||||
"featureFlagCapabilityFilterEnabledDescription": "જ્યારે લક્ષ્ય મોડેલમાં જરૂરી ક્ષમતાઓ (દૃષ્ટિ, સાધનો, રચિત આઉટપુટ, સંદર્ભ વિન્ડો) નથી ત્યારે વિતરણ પહેલાં વિનંતીઓને નકારી નાખો. કોમ્બો-લેયર સુસંગતતા ફિલ્ટરને બાયપાસ કરતી સીધી એકલ-પ્રદાતા વિનંતિઓને સુરક્ષિત કરે છે.",
|
||||
@@ -13850,5 +13921,13 @@
|
||||
"toolsMismatch": "પ્રદાતા ટૂલ કોલિંગને સપોર્ટ કરતો નથી",
|
||||
"structuredOutputMismatch": "પ્રદાતા સંરચિત આઉટપુટને સમર્થન આપતો નથી",
|
||||
"contextWindowMismatch": "વિનંતી પ્રદાતા સંદર્ભ વિન્ડોને પાર કરે છે"
|
||||
},
|
||||
"featureFlagDisableContextWindowChecksDescription": "Skip OmniRoute's local context-window and max-input-token check for direct single-model requests. Upstream providers still enforce their actual limits. Prompt compression and output-token caps remain active.",
|
||||
"cheaperInferenceSponsorBanner": {
|
||||
"title": "Cheaper Inference is an OmniRoute Open Source Friend",
|
||||
"description": "A cost-ranked gateway reselling dozens of frontier models behind one OpenAI-compatible endpoint — routing each request to the cheapest eligible provider, never above list price.",
|
||||
"cta": "Get an API Key",
|
||||
"partnerLinkNote": "Partner link",
|
||||
"dismissAriaLabel": "Dismiss"
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user