diff --git a/.gitignore b/.gitignore index fe81d4fe..57f710a5 100644 --- a/.gitignore +++ b/.gitignore @@ -85,4 +85,7 @@ graphify-out/* .script/ .codegraph/ .PR/ -.next-analyze/* \ No newline at end of file +.next-analyze/* + +# Kiro local workspace state +.kiro/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d729f06..da6b2c05 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,30 @@ +# v0.5.65 (2026-09-03) + +## Features +- **Fetch**: add Ollama Cloud web fetch provider +- **Gemini / Antigravity**: add Gemini 3.8 Flash support and bump IDE fingerprint to 2.11.0 +- **Claude**: add Claude Fable 5.1 support (adaptive thinking with `output_config.effort`), bump Claude Code fingerprint to 2.1.258 for new-model access +- **Providers**: add client-side status filter (All / Active / Inactive / No connection) on the Providers dashboard; add max height and scroll for connection list +- **Providers & Models**: streamline tokenrouter model catalog down to 22 flagship/newest models and add missing provider icons; refresh Codebuddy-CN catalog (add hy4-preview/hy3/glm-5.3/kimi-k3-1, drop EOL glm-5.0/glm-4.7) +- **Models**: capability toggles (vision, reasoning) when adding custom models with upsert and live caps refresh +- **CLI tools**: support saving and managing custom API key presets +- **Quota**: add usage and rate-limit tracking for Groq via `x-ratelimit-*` headers +- **i18n**: complete Indonesian translation (1391 keys) + +## Fixes +- **Security**: close SSRF guard bypasses in `ssrfGuard.js` (alternate IPv6 encodings, hostname trailing dots, wildcard DNS resolution check, safe redirect handling) (#3714) +- **Model markers**: strip the `[1m]` context marker Claude Code appends to model names (`claude-opus-5[1m]`) preventing model resolution failures (#3690) +- **Claude**: drop `server_tool_use` blocks carrying foreign IDs to avoid Anthropic 400 rejections; never anchor cache breakpoints on `defer_loading` tools (#3567) +- **Antigravity**: strike-break optimistic quota readings that keep 429ing by blocking the connection+model pair for 15m after 3 strikes (#3681); preserve client identity on model catalog requests (#3414) +- **Auth**: protect root `/responses` rewrite requiring API key validation in dashboardGuard +- **Chat & Docker**: return 503 Service Unavailable when all credentials are rate-limited; explicitly bundle `node-machine-id` into standalone Docker runtime image +- **OpenCode**: route Muse Spark models to `/zen/v1/responses` and declare vision support; filter inactive free model +- **Kiro**: preserve inline images as OpenAI-compatible `image_url` parts in OpenAI MITM; remove redundant top-level `systemPrompt` from payload +- **Usage**: read Responses-shape `cached_tokens` in `extractUsageFromResponse` for non-streaming traffic +- **Models**: support single model lookup with provider-prefixed IDs (e.g. `cc/claude-sonnet-5`) +- **Translator**: route Gemini thinking through `reasoning_effort` on OpenAI-compatible wire; convert `prefixItems` and ensure array items in Gemini schema sanitizer +- **UI**: apply persisted theme before first paint to prevent flash on reload; translate combo vision adapter label + # v0.5.59 (2026-08-29) ## Features @@ -40,6 +67,9 @@ - **i18n**: pt-BR expanded to 1132 terms ## Fixes +- **Claude Code**: add Claude Fable 5.1 and advertise Claude Code 2.1.258 in + both the request header and billing identity; use its permanent adaptive-thinking + mode with `output_config.effort` - **Stream**: record usage when a client closes on the terminal event — the Responses API has no [DONE] sentinel, so codex closed the socket on `response.completed` and cancelled the reader before flush() ran its usage @@ -738,4 +768,4 @@ # v0.4.46 (2026-05-15) ## Breaking Changes -- Tunnel public URL changed — old tunnel links no longer work, please reconnect to get the new URL \ No newline at end of file +- Tunnel public URL changed — old tunnel links no longer work, please reconnect to get the new URL diff --git a/Dockerfile b/Dockerfile index ad373f8a..b949f8f1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -40,6 +40,8 @@ COPY --from=builder /app/node_modules/next ./node_modules/next # sql.js loads dist/sql-wasm.wasm by path at runtime; tracing only follows JS imports, # so the last-resort DB driver would abort with ENOENT on the missing binary. COPY --from=builder /app/node_modules/sql.js ./node_modules/sql.js +# node-machine-id is createRequire-loaded at runtime; tracing omits it. +COPY --from=builder /app/node_modules/node-machine-id ./node_modules/node-machine-id RUN mkdir -p /app/data && chown -R node:node /app && \ mkdir -p /app/data-home && chown node:node /app/data-home && \ diff --git a/cli/package.json b/cli/package.json index a6618ce8..d5776b4e 100644 --- a/cli/package.json +++ b/cli/package.json @@ -1,6 +1,6 @@ { "name": "9router", - "version": "0.5.59", + "version": "0.5.65", "description": "9Router CLI - Start and manage 9Router server", "bin": { "9router": "./cli.js" diff --git a/cli/src/cli/menus/providers.js b/cli/src/cli/menus/providers.js index 92d10a04..42fbed6f 100644 --- a/cli/src/cli/menus/providers.js +++ b/cli/src/cli/menus/providers.js @@ -53,6 +53,9 @@ const PROVIDER_MODELS = { { id: "glm-4.7" }, ], ag: [ + { id: "gemini-3.8-flash-high" }, + { id: "gemini-3.8-flash-medium" }, + { id: "gemini-3.8-flash-low" }, { id: "gemini-3.7-flash-high" }, { id: "gemini-3.7-flash-medium" }, { id: "gemini-3.7-flash-low" }, @@ -101,6 +104,8 @@ const PROVIDER_MODELS = { { id: "claude-3-5-sonnet-20241022" }, ], gemini: [ + { id: "gemini-3.8-flash" }, + { id: "gemini-3.7-flash" }, { id: "gemini-3.6-flash" }, { id: "gemini-3.5-flash-lite" }, { id: "gemini-3-pro-preview" }, diff --git a/open-sse/config/providerModels.js b/open-sse/config/providerModels.js index 300fef5a..90a6b0b2 100644 --- a/open-sse/config/providerModels.js +++ b/open-sse/config/providerModels.js @@ -3,7 +3,8 @@ import REGISTRY from "../providers/registry/index.js"; // PROVIDER_MODELS now built from providers/registry (transport + models co-located) import { PROVIDER_MODELS } from "../providers/index.js"; import { modelQuotaFamily, modelStrip, modelTargetFormat, modelSupportedFormats, normalizeModelId } from "../providers/models/schema.js"; -import { CODEX_REVIEW_SUFFIX } from "../providers/models/helpers.js"; +import { CODEX_REVIEW_SUFFIX, isMuseSparkModel } from "../providers/models/helpers.js"; +import { FORMATS } from "../translator/formats.js"; export { PROVIDER_MODELS }; @@ -49,6 +50,9 @@ export function findModelName(aliasOrId, modelId) { } export function getModelTargetFormat(aliasOrId, modelId) { + if ((!aliasOrId || aliasOrId === "oc" || aliasOrId === "opencode") && isMuseSparkModel(modelId)) { + return FORMATS.OPENAI_RESPONSES; + } const models = PROVIDER_MODELS[aliasOrId]; if (!models) return null; return modelTargetFormat(findModel(models, modelId, aliasOrId)); diff --git a/open-sse/executors/opencode.js b/open-sse/executors/opencode.js index 381c47e8..ab14cb22 100644 --- a/open-sse/executors/opencode.js +++ b/open-sse/executors/opencode.js @@ -4,10 +4,14 @@ import { PROVIDERS } from "../config/providers.js"; import { getThinkingLevels } from "../providers/thinkingLevels.js"; import { injectReasoningContent } from "../utils/reasoningContentInjector.js"; import { resolveSessionId } from "../utils/sessionManager.js"; +import { isMuseSparkModel } from "../providers/models/helpers.js"; const OPENCODE_UA = "opencode"; // Models served by /zen/v1/responses; every other model stays on /chat/completions. -const RESPONSES_MODELS = new Set(["muse-spark-1.2-contributor-free"]); +const RESPONSES_MODELS = new Set([ + "muse-spark-1.2-contributor-free", + "muse-spark-1.3-contributor-free", +]); function generateRequestId() { return `msg_${crypto.randomUUID().replace(/-/g, "")}`; @@ -23,7 +27,8 @@ function baseModelId(model) { } function isResponsesModel(model) { - return RESPONSES_MODELS.has(baseModelId(model)); + const base = baseModelId(model); + return RESPONSES_MODELS.has(base) || isMuseSparkModel(base); } function resolveOpencodeSession(body, credentials) { diff --git a/open-sse/handlers/chatCore/requestDetail.js b/open-sse/handlers/chatCore/requestDetail.js index 376d5f90..1861e995 100644 --- a/open-sse/handlers/chatCore/requestDetail.js +++ b/open-sse/handlers/chatCore/requestDetail.js @@ -25,10 +25,16 @@ export function extractUsageFromResponse(responseBody) { if (!responseBody || typeof responseBody !== "object") return null; // Claude format + // Note: OpenAI Responses usage ({input_tokens, input_tokens_details:{cached_tokens}}) + // also matches this branch. Its prompt is cache-INCLUSIVE and its cache rides in + // input_tokens_details, so emit it as cached_tokens — the convention + // canonicalizeUsage() passes through without folding. Reading it here keeps + // cache accounting correct for /v1/responses and codex traffic. if (responseBody.usage?.input_tokens !== undefined) { return { prompt_tokens: responseBody.usage.input_tokens || 0, completion_tokens: responseBody.usage.output_tokens || 0, + cached_tokens: responseBody.usage.cached_tokens ?? responseBody.usage.input_tokens_details?.cached_tokens, cache_read_input_tokens: responseBody.usage.cache_read_input_tokens, cache_creation_input_tokens: responseBody.usage.cache_creation_input_tokens }; @@ -39,7 +45,7 @@ export function extractUsageFromResponse(responseBody) { return { prompt_tokens: responseBody.usage.prompt_tokens || 0, completion_tokens: responseBody.usage.completion_tokens || 0, - cached_tokens: responseBody.usage.prompt_tokens_details?.cached_tokens, + cached_tokens: responseBody.usage.cached_tokens ?? responseBody.usage.prompt_tokens_details?.cached_tokens, reasoning_tokens: responseBody.usage.completion_tokens_details?.reasoning_tokens }; } diff --git a/open-sse/handlers/fetch/index.js b/open-sse/handlers/fetch/index.js index 187bd60e..9bad7e18 100644 --- a/open-sse/handlers/fetch/index.js +++ b/open-sse/handlers/fetch/index.js @@ -1,4 +1,4 @@ -// Web Fetch handler — dispatches to firecrawl, jina-reader, tavily, exa +// Web Fetch handler — dispatches to firecrawl, jina-reader, tavily, exa, ollama // Returns normalized shape across all providers const DEFAULT_TIMEOUT_MS = 15000; @@ -56,8 +56,8 @@ function parseJinaTitle(text) { return m ? m[1].trim() : null; } -function buildData({ provider, url, title, format, text, costUsd, responseMs, upstreamMs }) { - return { +function buildData({ provider, url, title, format, text, links, costUsd, responseMs, upstreamMs }) { + const data = { provider, url, title: title || null, @@ -66,6 +66,8 @@ function buildData({ provider, url, title, format, text, costUsd, responseMs, up usage: { fetch_cost_usd: costUsd ?? null }, metrics: { response_time_ms: responseMs, upstream_latency_ms: upstreamMs } }; + if (Array.isArray(links)) data.links = links; + return data; } async function readJsonOrText(res) { @@ -115,6 +117,18 @@ export async function handleFetchCore({ url, format, maxCharacters, provider, pr if (provider === "exa") { return await runExa({ url, fmt, timeoutMs, apiKey, maxCharacters, costPerQuery, startedAt }); } + if (provider === "ollama") { + return await runOllama({ + url, + fmt, + timeoutMs, + apiKey, + maxCharacters, + costPerQuery, + startedAt, + baseUrl: providerConfig?.baseUrl, + }); + } return { success: false, status: 400, error: `Unsupported provider: ${provider}` }; } catch (err) { log?.("fetch handler error:", err?.message || err); @@ -241,3 +255,56 @@ async function runExa({ url, fmt, timeoutMs, apiKey, maxCharacters, costPerQuery }) }; } + +async function runOllama({ + url, + fmt, + timeoutMs, + apiKey, + maxCharacters, + costPerQuery, + startedAt, + baseUrl, +}) { + const upstreamStart = Date.now(); + const r = await tryFetch(baseUrl, { + method: "POST", + headers: { + "content-type": "application/json", + ...(apiKey ? { authorization: `Bearer ${apiKey}` } : {}) + }, + body: JSON.stringify({ url }) + }, timeoutMs); + + if (!r.ok) { + return { success: false, status: r.timeout ? 504 : 502, error: r.error }; + } + const upstreamMs = Date.now() - upstreamStart; + const { json, text: responseText } = await readJsonOrText(r.res); + if (!r.res.ok) { + const error = json?.error + || json?.message + || responseText?.slice(0, 500) + || `Ollama error: ${r.res.status}`; + return { success: false, status: r.res.status, error }; + } + if (!json || typeof json.content !== "string") { + return { success: false, status: 502, error: "Ollama returned an empty or invalid web fetch response" }; + } + + const text = truncate(json.content, maxCharacters); + return { + success: true, + data: buildData({ + provider: "ollama", + url, + title: json.title || null, + format: fmt, + text, + links: json.links, + costUsd: costPerQuery, + responseMs: Date.now() - startedAt, + upstreamMs + }) + }; +} diff --git a/open-sse/handlers/search/index.js b/open-sse/handlers/search/index.js index 662b293e..70c3c749 100644 --- a/open-sse/handlers/search/index.js +++ b/open-sse/handlers/search/index.js @@ -10,6 +10,7 @@ import { buildSearchRequest } from "./callers.js"; import { normalizeSearchResponse } from "./normalizers.js"; import { handleChatSearch } from "./chatSearch.js"; +import { fetchPublic } from "../../../src/shared/utils/ssrfGuard.js"; const GLOBAL_TIMEOUT_MS = 15000; const NON_RETRIABLE = new Set([400, 401, 403, 404]); @@ -100,7 +101,7 @@ async function tryDedicatedProvider({ provider, providerConfig, body, credential log?.info?.("SEARCH", `${provider.id} | "${params.query.slice(0, 80)}" | type=${params.searchType}`); try { - const resp = await fetch(url, { ...init, headers: sanitizeHeaders(init.headers), signal: controller.signal }); + const resp = await fetchPublic(url, { ...init, headers: sanitizeHeaders(init.headers), signal: controller.signal }); clearTimeout(timer); if (!resp.ok) { const errText = await resp.text().catch(() => ""); diff --git a/open-sse/providers/capabilities.js b/open-sse/providers/capabilities.js index dcdac7ea..5a0b7271 100644 --- a/open-sse/providers/capabilities.js +++ b/open-sse/providers/capabilities.js @@ -83,7 +83,8 @@ export function capabilitiesFromServiceKind(kind) { * otherwise mis-match. Only declare deltas vs DEFAULT. */ export const MODEL_CAPABILITIES = { - // Claude Opus 5, 4.6/4.7/4.8, and Kiro Sonnet 5 have 1M context + adaptive thinking (override generic claude pattern) + // Claude Fable 5.1, Opus 5, 4.6/4.7/4.8, and Kiro Sonnet 5 have 1M context + adaptive thinking (override generic claude pattern) + "claude-fable-5-1": { vision: true, reasoning: true, search: true, thinkingFormat: "claude-adaptive", thinkingCanDisable: false, contextWindow: 1000000, maxOutput: 128000 }, "claude-opus-5": { vision: true, reasoning: true, search: true, thinkingFormat: "claude-adaptive", contextWindow: 1000000, maxOutput: 128000 }, "claude-opus-5-thinking": { vision: true, reasoning: true, search: true, thinkingFormat: "claude-adaptive", contextWindow: 1000000, maxOutput: 128000 }, "claude-opus-5-agentic": { vision: true, reasoning: true, search: true, thinkingFormat: "claude-adaptive", contextWindow: 1000000, maxOutput: 128000 }, @@ -126,8 +127,10 @@ export const MODEL_CAPABILITIES = { "kimi-for-coding-highspeed": { vision: true, videoInput: true, reasoning: true, thinkingFormat: "kimi", thinkingCanDisable: false, contextWindow: 262144, maxOutput: 65536 }, "kimi-k2.7-code": { vision: true, videoInput: true, reasoning: true, thinkingFormat: "kimi", thinkingCanDisable: false, contextWindow: 262144, maxOutput: 65536 }, "kimi-k2.7-code-highspeed": { vision: true, videoInput: true, reasoning: true, thinkingFormat: "kimi", thinkingCanDisable: false, contextWindow: 262144, maxOutput: 65536 }, - // OpenCode Free Muse Spark — OpenAI Responses reasoning supports up to xhigh. - "muse-spark-1.2-contributor-free": { reasoning: true, thinkingFormat: "openai", contextWindow: 1048576, maxOutput: 131072 }, + // OpenCode Free Muse Spark — multimodal (text+image per models.dev meta/muse-spark) + // via OpenAI Responses input_image; reasoning supports up to xhigh. + "muse-spark-1.2-contributor-free": { vision: true, reasoning: true, thinkingFormat: "openai", contextWindow: 1048576, maxOutput: 131072 }, + "muse-spark-1.3-contributor-free": { vision: true, reasoning: true, thinkingFormat: "openai", contextWindow: 1048576, maxOutput: 131072 }, }; const KIRO_GPT_5_6_CAPABILITIES = { vision: true, reasoning: true, search: true, thinkingFormat: "openai", contextWindow: 272000, maxOutput: 128000 }; @@ -190,6 +193,15 @@ export const PROVIDER_CAPABILITIES = { "kimi-k2.6": { vision: true, reasoning: true, thinkingFormat: "openai", thinkingCanDisable: false, contextWindow: 256000, maxOutput: 32000 }, "kimi-k2.5": { vision: true, reasoning: true, thinkingFormat: "openai", thinkingCanDisable: false, contextWindow: 164000, maxOutput: 32000 }, "hy3-preview": { vision: true, reasoning: true, thinkingFormat: "openai", thinkingCanDisable: false, contextWindow: 192000, maxOutput: 64000 }, + // hy3/hy3-x: 256K official (192K conservative, matches hy3-preview); hy4-preview: 1M official. + // glm-5.3: 1M (GLM-5.x gen); glm-5.3-flash window unverified (200K conservative). + "hy3": { vision: true, reasoning: true, thinkingFormat: "openai", thinkingCanDisable: false, contextWindow: 192000, maxOutput: 64000 }, + "hy3-x": { vision: true, reasoning: true, thinkingFormat: "openai", thinkingCanDisable: false, contextWindow: 192000, maxOutput: 64000 }, + "hy4-preview": { vision: true, reasoning: true, thinkingFormat: "openai", thinkingCanDisable: false, contextWindow: 1000000, maxOutput: 64000 }, + "hy4-preview-x": { vision: true, reasoning: true, thinkingFormat: "openai", thinkingCanDisable: false, contextWindow: 1000000, maxOutput: 64000 }, + "glm-5.3": { reasoning: true, thinkingFormat: "openai", thinkingCanDisable: false, contextWindow: 1000000, maxOutput: 48000 }, + "glm-5.3-flash": { reasoning: true, thinkingFormat: "openai", thinkingCanDisable: false, contextWindow: 200000, maxOutput: 48000 }, + "kimi-k3-1": { vision: true, reasoning: true, thinkingFormat: "openai", thinkingCanDisable: false, contextWindow: 256000, maxOutput: 32000 }, "deepseek-v4-pro": { vision: true, reasoning: true, thinkingFormat: "openai", thinkingCanDisable: false, contextWindow: 1000000, maxOutput: 50000 }, "deepseek-v4-flash": { vision: true, reasoning: true, thinkingFormat: "openai", thinkingCanDisable: false, contextWindow: 1000000, maxOutput: 50000 }, "deepseek-v3-2-volc": { reasoning: true, thinkingFormat: "openai", thinkingCanDisable: false, contextWindow: 96000, maxOutput: 32000 }, @@ -225,6 +237,7 @@ export const PATTERN_CAPABILITIES = [ // ── Gemini (all 2.0+ multimodal + google_search grounding, 1M ctx) ─ { pattern: "*gemini*image*", caps: { vision: true, imageOutput: true, contextWindow: 1048576 } }, + { pattern: "*gemini-3.8*", caps: { vision: true, audioInput: true, videoInput: true, reasoning: true, search: true, thinkingFormat: "gemini-level", thinkingCanDisable: false, contextWindow: 1048576, maxOutput: 65536 } }, { pattern: "*gemini-3.7*", caps: { vision: true, audioInput: true, videoInput: true, reasoning: true, search: true, thinkingFormat: "gemini-level", thinkingCanDisable: false, contextWindow: 1048576, maxOutput: 65536 } }, { pattern: "*gemini-3*pro*", caps: { vision: true, audioInput: true, videoInput: true, reasoning: true, search: true, thinkingFormat: "gemini-level", thinkingCanDisable: false, contextWindow: 1048576, maxOutput: 65535 } }, { pattern: "*gemini-3*", caps: { vision: true, audioInput: true, videoInput: true, reasoning: true, search: true, thinkingFormat: "gemini-level", thinkingCanDisable: false, contextWindow: 1048576, maxOutput: 65536 } }, @@ -335,6 +348,9 @@ export const PATTERN_CAPABILITIES = [ { pattern: "*laguna-s-2.1*", caps: { reasoning: true, thinkingFormat: "openai", contextWindow: 1000000, maxOutput: 32000 } }, { pattern: "*laguna*", caps: { reasoning: true, thinkingFormat: "openai", contextWindow: 200000, maxOutput: 32000 } }, + + // ── OpenCode Free Muse Spark (multimodal text+image; OpenAI Responses reasoning supports up to xhigh) ─ + { pattern: "*muse*spark*", caps: { vision: true, reasoning: true, thinkingFormat: "openai", contextWindow: 1048576, maxOutput: 131072 } }, // ── Others ─────────────────────────────────────────────────────── { pattern: "*hunyuan*", caps: { reasoning: true, thinkingFormat: "hunyuan", contextWindow: 262144, maxOutput: 262144 } }, { pattern: "hy3*", caps: { reasoning: true, thinkingFormat: "hunyuan", contextWindow: 262144, maxOutput: 262144 } }, diff --git a/open-sse/providers/models/helpers.js b/open-sse/providers/models/helpers.js index a7d273d6..c66d662b 100644 --- a/open-sse/providers/models/helpers.js +++ b/open-sse/providers/models/helpers.js @@ -18,3 +18,10 @@ export function withCodexReviewModels(models) { ]; }); } + +export function isMuseSparkModel(modelId) { + if (!modelId || typeof modelId !== "string") return false; + const clean = modelId.replace(/\([^()]+\)\s*$/, "").trim(); + const base = clean.includes("/") ? clean.split("/").pop() : clean; + return /^muse[-_]?spark(?:$|[-_:.\s])/i.test(base); +} diff --git a/open-sse/providers/pricing.js b/open-sse/providers/pricing.js index cf6ffd55..7cc302ff 100644 --- a/open-sse/providers/pricing.js +++ b/open-sse/providers/pricing.js @@ -57,6 +57,10 @@ export const MODEL_PRICING = { "o1-mini": { input: 3.00, output: 12.00, cached: 1.50, reasoning: 18.00, cache_creation: 3.00 }, // === Gemini === + "gemini-3.8-flash": { input: 1.50, output: 7.50, cached: 0.15, reasoning: 11.25, cache_creation: 1.875 }, + "gemini-3.8-flash-high": { input: 1.50, output: 7.50, cached: 0.15, reasoning: 11.25, cache_creation: 1.875 }, + "gemini-3.8-flash-medium": { input: 1.50, output: 7.50, cached: 0.15, reasoning: 11.25, cache_creation: 1.875 }, + "gemini-3.8-flash-low": { input: 1.50, output: 7.50, cached: 0.15, reasoning: 11.25, cache_creation: 1.875 }, "gemini-3.7-flash": { input: 1.50, output: 7.50, cached: 0.15, reasoning: 11.25, cache_creation: 1.875 }, "gemini-3.7-flash-high": { input: 1.50, output: 7.50, cached: 0.15, reasoning: 11.25, cache_creation: 1.875 }, "gemini-3.7-flash-medium": { input: 1.50, output: 7.50, cached: 0.15, reasoning: 11.25, cache_creation: 1.875 }, @@ -260,6 +264,7 @@ export const PROVIDER_PRICING = { "z-ai/glm-5-turbo": { input: 1.2, output: 4.0, cached: 0.24, reasoning: 4.0 }, "z-ai/glm-5.1": { input: 1.05, output: 3.5, cached: 0.525, reasoning: 3.5 }, "z-ai/glm-5.2": { input: 1.4, output: 4.4, cached: 0.26, reasoning: 4.4 }, + "z-ai/glm-5.3-free": { input: 0, output: 0, cached: 0, reasoning: 0 }, }, }; diff --git a/open-sse/providers/registry/antigravity.js b/open-sse/providers/registry/antigravity.js index 1f14f415..ab1ee574 100644 --- a/open-sse/providers/registry/antigravity.js +++ b/open-sse/providers/registry/antigravity.js @@ -36,8 +36,7 @@ export default { }, }, usage: { - // Discovery (quota/project) on PROD; daily host rejects these. - quotaApiUrl: "https://cloudcode-pa.googleapis.com/v1internal:fetchAvailableModels", + quotaApiUrl: `${ANTIGRAVITY_IDE_BASE_URL}/v1internal:fetchAvailableModels`, loadProjectApiUrl: "https://cloudcode-pa.googleapis.com/v1internal:loadCodeAssist", tokenUrl: "https://oauth2.googleapis.com/token", }, @@ -45,6 +44,10 @@ export default { clientSecret: "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", }, models: [ + { id: "gemini-3.8-flash-high", name: "Gemini 3.8 Flash (High)", upstreamModelId: "gemini-3.8-flash-high(high)" }, + { id: "gemini-3.8-flash-medium", name: "Gemini 3.8 Flash (Medium)", upstreamModelId: "gemini-3.8-flash-medium(medium)" }, + { id: "gemini-3.8-flash-low", name: "Gemini 3.8 Flash (Low)", upstreamModelId: "gemini-3.8-flash-low(low)" }, + { id: "gemini-3.8-flash", name: "Gemini 3.8 Flash", upstreamModelId: "gemini-3.8-flash-medium(medium)" }, { id: "gemini-3.7-flash-high", name: "Gemini 3.7 Flash (High)", upstreamModelId: "gemini-3.7-flash-tiered(high)" }, { id: "gemini-3.7-flash-medium", name: "Gemini 3.7 Flash (Medium)", upstreamModelId: "gemini-3.7-flash-tiered(medium)" }, { id: "gemini-3.7-flash-low", name: "Gemini 3.7 Flash (Low)", upstreamModelId: "gemini-3.7-flash-tiered(low)" }, diff --git a/open-sse/providers/registry/claude.js b/open-sse/providers/registry/claude.js index 8c47a6b3..428e66ad 100644 --- a/open-sse/providers/registry/claude.js +++ b/open-sse/providers/registry/claude.js @@ -1,4 +1,4 @@ -import { CLAUDE_CLI_SPOOF_HEADERS } from "../shared.js"; +import { CLAUDE_CLI_VERSION } from "../shared.js"; export default { id: "claude", @@ -25,7 +25,7 @@ export default { "Anthropic-Version": "2023-06-01", "Anthropic-Beta": "claude-code-20250219,oauth-2025-04-20,interleaved-thinking-2025-05-14,context-management-2025-06-27,prompt-caching-scope-2026-01-05,advanced-tool-use-2025-11-20,effort-2025-11-24,structured-outputs-2025-12-15,fast-mode-2026-02-01,redact-thinking-2026-02-12,token-efficient-tools-2026-03-28", "Anthropic-Dangerous-Direct-Browser-Access": "true", - "User-Agent": "claude-cli/2.1.92 (external, sdk-cli)", + "User-Agent": `claude-cli/${CLAUDE_CLI_VERSION} (external, sdk-cli)`, "X-App": "cli", "X-Stainless-Helper-Method": "stream", "X-Stainless-Retry-Count": "0", @@ -58,6 +58,7 @@ export default { }, models: [ { id: "claude-opus-5", name: "Claude Opus 5" }, + { id: "claude-fable-5-1", name: "Claude Fable 5.1" }, { id: "claude-fable-5", name: "Claude Fable 5" }, { id: "claude-sonnet-5", name: "Claude Sonnet 5" }, { id: "claude-haiku-4-5-20251001", name: "Claude 4.5 Haiku" }, diff --git a/open-sse/providers/registry/codebuddy-cn.js b/open-sse/providers/registry/codebuddy-cn.js index 01a78a30..33345b9f 100644 --- a/open-sse/providers/registry/codebuddy-cn.js +++ b/open-sse/providers/registry/codebuddy-cn.js @@ -47,16 +47,24 @@ export default { models: [ { id: "glm-5.2", name: "GLM-5.2" }, { id: "glm-5.1", name: "GLM-5.1" }, - { id: "glm-5.0", name: "GLM-5.0" }, { id: "glm-5.0-turbo", name: "GLM-5.0-Turbo" }, { id: "glm-5v-turbo", name: "GLM-5v-Turbo" }, - { id: "glm-4.7", name: "GLM-4.7" }, { id: "minimax-m3", name: "MiniMax-M3" }, { id: "minimax-m2.7", name: "MiniMax-M2.7" }, { id: "kimi-k2.7", name: "Kimi-K2.7-Code" }, { id: "kimi-k2.6", name: "Kimi-K2.6" }, { id: "kimi-k2.5", name: "Kimi-K2.5" }, + // "-x" suffix = paid tier of the same model (free id rides the promo quota: + // hy3 free until 2026-08-31, hy4-preview until 2026-09-10). Server model table + // seen in client logs 2026-08-30; glm-5.0 / glm-4.7 removed (API 11102 dead). { id: "hy3-preview", name: "Hy3 Preview" }, + { id: "hy3", name: "Hy3" }, + { id: "hy3-x", name: "Hy3 (Paid)" }, + { id: "hy4-preview", name: "Hy4-Preview" }, + { id: "hy4-preview-x", name: "Hy4-Preview (Paid)" }, + { id: "glm-5.3", name: "GLM-5.3" }, + { id: "glm-5.3-flash", name: "GLM-5.3-Flash" }, + { id: "kimi-k3-1", name: "Kimi-K3" }, { id: "deepseek-v4-pro", name: "DeepSeek-V4-Pro" }, { id: "deepseek-v4-flash", name: "DeepSeek-V4-Flash" }, { id: "deepseek-v3-2-volc", name: "DeepSeek-V3.2" }, diff --git a/open-sse/providers/registry/gemini.js b/open-sse/providers/registry/gemini.js index df643556..590bf48e 100644 --- a/open-sse/providers/registry/gemini.js +++ b/open-sse/providers/registry/gemini.js @@ -36,6 +36,7 @@ export default { }, }, models: [ + { id: "gemini-3.8-flash", name: "Gemini 3.8 Flash" }, { id: "gemini-3.7-flash", name: "Gemini 3.7 Flash" }, { id: "gemini-3.6-flash", name: "Gemini 3.6 Flash" }, { id: "gemini-3.5-flash-lite", name: "Gemini 3.5 Flash Lite" }, diff --git a/open-sse/providers/registry/groq.js b/open-sse/providers/registry/groq.js index 2ad8a6d8..b1c04538 100644 --- a/open-sse/providers/registry/groq.js +++ b/open-sse/providers/registry/groq.js @@ -17,6 +17,12 @@ export default { transport: { baseUrl: "https://api.groq.com/openai/v1/chat/completions", validateUrl: "https://api.groq.com/openai/v1/models", + // No dedicated quota endpoint; rate-limit info rides on x-ratelimit-* + // response headers, always included. Reuse the models list (already + // used as validateUrl) so reading usage never costs tokens. + usage: { + url: "https://api.groq.com/openai/v1/models", + }, }, models: [ { id: "llama-3.3-70b-versatile", name: "Llama 3.3 70B" }, @@ -34,4 +40,8 @@ export default { authHeader: "bearer", format: "openai", }, + features: { + usage: true, + usageApikey: true, + }, }; diff --git a/open-sse/providers/registry/ollama.js b/open-sse/providers/registry/ollama.js index 89fec43c..6965484a 100644 --- a/open-sse/providers/registry/ollama.js +++ b/open-sse/providers/registry/ollama.js @@ -31,7 +31,16 @@ export default { { id: "qwen3.5", name: "Qwen3.5" }, { id: "minimax-m3", name: "MiniMax M3" }, ], - serviceKinds: ["llm"], + serviceKinds: ["llm", "webFetch"], + fetchConfig: { + baseUrl: "https://ollama.com/api/web_fetch", + method: "POST", + authType: "apikey", + authHeader: "bearer", + formats: ["markdown"], + maxCharacters: 200000, + timeoutMs: 30000, + }, features: { usage: true, usageApikey: true, diff --git a/open-sse/providers/registry/opencode.js b/open-sse/providers/registry/opencode.js index 469c64ca..03e72eef 100644 --- a/open-sse/providers/registry/opencode.js +++ b/open-sse/providers/registry/opencode.js @@ -20,9 +20,10 @@ export default { noAuth: true, }, models: [ - // Only this model is served by /zen/v1/responses; the rest stay on + // Muse Spark models are served by /zen/v1/responses; the rest stay on // /chat/completions, so the format is declared per-model, not per-provider. { id: "muse-spark-1.2-contributor-free", name: "Muse Spark 1.2 Contributor Free", targetFormat: "openai-responses" }, + { id: "muse-spark-1.3-contributor-free", name: "Muse Spark 1.3 Contributor Free", targetFormat: "openai-responses" }, ], modelsFetcher: { url: "https://opencode.ai/zen/v1/models", type: "opencode-free" }, passthroughModels: true, diff --git a/open-sse/providers/registry/tokenrouter.js b/open-sse/providers/registry/tokenrouter.js index 2e3cd248..4985b3fd 100644 --- a/open-sse/providers/registry/tokenrouter.js +++ b/open-sse/providers/registry/tokenrouter.js @@ -24,129 +24,31 @@ export default { validateUrl: "https://api.tokenrouter.com/v1/models", thinkingFormat: "tokenrouter", }, - // Seed snapshot from live /v1/models (120 entries). Latest catalogue is + // Seed snapshot from live /v1/models. Latest catalogue is // fetched via modelsFetcher; other ids still accepted via passthroughModels. models: [ - { id: "MiniMax-Hailuo-2.3", name: "Minimax Hailuo 2.3", kind: "video" }, - { id: "MiniMax-M3", name: "Minimax M3" }, - { id: "anthropic/claude-fable-5", name: "Claude Fable 5" }, { id: "anthropic/claude-haiku-4.5", name: "Claude Haiku 4.5" }, - { id: "anthropic/claude-opus-4.5", name: "Claude Opus 4.5" }, - { id: "anthropic/claude-opus-4.6", name: "Claude Opus 4.6" }, - { id: "anthropic/claude-opus-4.7", name: "Claude Opus 4.7" }, - { id: "anthropic/claude-opus-4.7-fast", name: "Claude Opus 4.7 Fast" }, + { id: "anthropic/claude-sonnet-4.6", name: "Claude Sonnet 4.6" }, { id: "anthropic/claude-opus-4.8", name: "Claude Opus 4.8" }, { id: "anthropic/claude-opus-4.8-fast", name: "Claude Opus 4.8 Fast" }, - { id: "anthropic/claude-opus-5", name: "Claude Opus 5" }, - { id: "anthropic/claude-opus-5-fast", name: "Claude Opus 5 Fast" }, - { id: "anthropic/claude-sonnet-4", name: "Claude Sonnet 4" }, - { id: "anthropic/claude-sonnet-4.5", name: "Claude Sonnet 4.5" }, - { id: "anthropic/claude-sonnet-4.6", name: "Claude Sonnet 4.6" }, - { id: "anthropic/claude-sonnet-5", name: "Claude Sonnet 5" }, - { id: "bytedance-seed/seedream-4.5", name: "Seedream 4.5", kind: "image" }, - { id: "bytedance-seed/seedream-5.0-lite", name: "Seedream 5.0 Lite", kind: "image" }, - { id: "bytedance-seed/seedream-5.0-pro", name: "Seedream 5.0 Pro", kind: "image" }, - { id: "claude-haiku-4-5", name: "Claude Haiku 4 5" }, - { id: "claude-opus-4-8-m-aws", name: "Claude Opus 4 8 M Aws" }, - { id: "deepseek/deepseek-v3.2", name: "Deepseek V3.2" }, - { id: "deepseek/deepseek-v4-flash", name: "Deepseek V4 Flash" }, - { id: "deepseek/deepseek-v4-flash-0731", name: "Deepseek V4 Flash 0731" }, - { id: "deepseek/deepseek-v4-pro", name: "Deepseek V4 Pro" }, - { id: "ex/gpt-5.4", name: "Gpt 5.4" }, - { id: "google/gemini-2.5-flash-image", name: "Gemini 2.5 Flash Image" }, - { id: "google/gemini-3-flash-preview", name: "Gemini 3 Flash Preview" }, - { id: "google/gemini-3-pro-image-preview", name: "Gemini 3 Pro Image Preview" }, - { id: "google/gemini-3.1-flash-image-preview", name: "Gemini 3.1 Flash Image Preview" }, - { id: "google/gemini-3.1-flash-lite-image", name: "Gemini 3.1 Flash Lite Image" }, - { id: "google/gemini-3.1-pro-preview", name: "Gemini 3.1 Pro Preview" }, - { id: "google/gemini-3.5-flash", name: "Gemini 3.5 Flash" }, - { id: "google/gemini-3.5-flash-lite", name: "Gemini 3.5 Flash Lite" }, - { id: "google/gemini-3.6-flash", name: "Gemini 3.6 Flash" }, - { id: "google/gemini-embedding-2", name: "Gemini Embedding 2" }, - { id: "google/gemma-4-26b-a4b-it", name: "Gemma 4 26B A4B It" }, - { id: "happyhorse-1.0-t2v", name: "Happyhorse 1.0 T2V", kind: "video" }, - { id: "kling-3.0-turbo", name: "Kling 3.0 Turbo", kind: "video" }, - { id: "kling-v2-6", name: "Kling V2 6", kind: "video" }, - { id: "kling-v3", name: "Kling V3", kind: "video" }, - { id: "kling-v3-omni", name: "Kling V3 Omni", kind: "video" }, - { id: "microsoft/mai-image-2.5", name: "Mai Image 2.5" }, - { id: "minimax/minimax-m2-her", name: "Minimax M2 Her" }, - { id: "minimax/minimax-m2.1", name: "Minimax M2.1" }, - { id: "minimax/minimax-m2.1-highspeed", name: "Minimax M2.1 Highspeed" }, - { id: "minimax/minimax-m2.5", name: "Minimax M2.5" }, - { id: "minimax/minimax-m2.7", name: "Minimax M2.7" }, - { id: "minimax/minimax-m2.7-highspeed", name: "Minimax M2.7 Highspeed" }, - { id: "miromind/mirothinker-1-7-deepresearch", name: "Mirothinker 1 7 Deepresearch" }, - { id: "miromind/mirothinker-1-7-deepresearch-mini", name: "Mirothinker 1 7 Deepresearch Mini" }, - { id: "mistralai/devstral-2512", name: "Devstral 2512" }, - { id: "mistralai/mistral-medium-3-5", name: "Mistral Medium 3 5" }, - { id: "mistralai/mistral-small-2603", name: "Mistral Small 2603" }, - { id: "mistralai/voxtral-small-24b-2507", name: "Voxtral Small 24B 2507" }, - { id: "moonshotai/kimi-k2.5", name: "Kimi K2.5" }, - { id: "moonshotai/kimi-k2.6", name: "Kimi K2.6" }, - { id: "moonshotai/kimi-k2.7-code", name: "Kimi K2.7 Code" }, - { id: "moonshotai/kimi-k3", name: "Kimi K3" }, - { id: "moonshotai/kimi-k3-free", name: "Kimi K3 Free" }, - { id: "nvidia/nemotron-3-nano-omni-30b-a3b-reasoning:free", name: "Nemotron 3 Nano Omni 30B A3B Reasoning:Free" }, - { id: "nvidia/nemotron-3-super-120b-a12b", name: "Nemotron 3 Super 120B A12B" }, - { id: "openai/gpt-4o-mini", name: "Gpt 4O Mini" }, - { id: "openai/gpt-5", name: "Gpt 5" }, - { id: "openai/gpt-5-image", name: "Gpt 5 Image" }, - { id: "openai/gpt-5-image-mini", name: "Gpt 5 Image Mini" }, - { id: "openai/gpt-5-mini", name: "Gpt 5 Mini" }, - { id: "openai/gpt-5.2", name: "Gpt 5.2" }, { id: "openai/gpt-5.4", name: "Gpt 5.4" }, - { id: "openai/gpt-5.4-image-2", name: "Gpt 5.4 Image 2", kind: "image" }, { id: "openai/gpt-5.4-mini", name: "Gpt 5.4 Mini" }, - { id: "openai/gpt-5.4-nano", name: "Gpt 5.4 Nano" }, { id: "openai/gpt-5.4-pro", name: "Gpt 5.4 Pro" }, { id: "openai/gpt-5.5", name: "Gpt 5.5" }, - { id: "openai/gpt-5.5-pro", name: "Gpt 5.5 Pro" }, - { id: "openai/gpt-5.6-luna", name: "Gpt 5.6 Luna" }, { id: "openai/gpt-5.6-sol", name: "Gpt 5.6 Sol" }, - { id: "openai/gpt-5.6-terra", name: "Gpt 5.6 Terra" }, - { id: "openai/gpt-audio", name: "Gpt Audio", kind: "audio" }, - { id: "openai/gpt-audio-mini", name: "Gpt Audio Mini", kind: "audio" }, - { id: "openai/gpt-oss-120b", name: "Gpt Oss 120B" }, + { id: "google/gemini-3.5-flash", name: "Gemini 3.5 Flash" }, + { id: "google/gemini-3.6-flash", name: "Gemini 3.6 Flash" }, + { id: "deepseek/deepseek-v4-flash", name: "Deepseek V4 Flash" }, + { id: "deepseek/deepseek-v4-pro", name: "Deepseek V4 Pro" }, { id: "qwen/qwen3-coder-next", name: "Qwen3 Coder Next" }, - { id: "qwen/qwen3.5-122b-a10b", name: "Qwen3.5 122B A10B" }, - { id: "qwen/qwen3.5-35b-a3b", name: "Qwen3.5 35B A3B" }, - { id: "qwen/qwen3.5-397b-a17b", name: "Qwen3.5 397B A17B" }, - { id: "qwen/qwen3.5-9b", name: "Qwen3.5 9B" }, - { id: "qwen/qwen3.5-flash", name: "Qwen3.5 Flash" }, - { id: "qwen/qwen3.5-plus-02-15", name: "Qwen3.5 Plus 02 15" }, - { id: "qwen/qwen3.6-plus", name: "Qwen3.6 Plus" }, { id: "qwen/qwen3.7-max", name: "Qwen3.7 Max" }, - { id: "qwen/qwen3.7-plus", name: "Qwen3.7 Plus" }, { id: "qwen/qwen3.8-max", name: "Qwen3.8 Max" }, - { id: "qwen3.5-omni-plus", name: "Qwen3.5 Omni Plus" }, - { id: "qwen3.6-flash", name: "Qwen3.6 Flash" }, - { id: "sakana/fugu-ultra", name: "Fugu Ultra" }, - { id: "seed-2-0-code-preview-260328", name: "Seed 2 0 Code Preview 260328" }, - { id: "seed-2-0-lite-260428", name: "Seed 2 0 Lite 260428" }, - { id: "seed-2-0-mini-260428", name: "Seed 2 0 Mini 260428" }, - { id: "seed-2-0-pro-260328", name: "Seed 2 0 Pro 260328" }, - { id: "stepfun/step-3.5-flash", name: "Step 3.5 Flash" }, - { id: "stepfun/step-3.7-flash", name: "Step 3.7 Flash" }, - { id: "tencent/hy3-preview", name: "Hy3 Preview" }, - { id: "x-ai/grok-4.1-fast", name: "Grok 4.1 Fast" }, - { id: "x-ai/grok-4.20-beta", name: "Grok 4.20 Beta" }, - { id: "x-ai/grok-4.3", name: "Grok 4.3" }, - { id: "x-ai/grok-4.5", name: "Grok 4.5" }, - { id: "x-ai/grok-build-0.1", name: "Grok Build 0.1" }, - { id: "xiaomi/mimo-v2-flash", name: "Mimo V2 Flash" }, - { id: "xiaomi/mimo-v2-omni", name: "Mimo V2 Omni" }, - { id: "xiaomi/mimo-v2-pro", name: "Mimo V2 Pro" }, - { id: "xiaomi/mimo-v2.5", name: "Mimo V2.5" }, - { id: "xiaomi/mimo-v2.5-pro", name: "Mimo V2.5 Pro" }, - { id: "z-ai/glm-4.5-air", name: "Glm 4.5 Air" }, - { id: "z-ai/glm-4.6", name: "Glm 4.6" }, - { id: "z-ai/glm-4.6v", name: "Glm 4.6V" }, - { id: "z-ai/glm-4.7", name: "Glm 4.7" }, - { id: "z-ai/glm-5", name: "Glm 5" }, - { id: "z-ai/glm-5-turbo", name: "Glm 5 Turbo" }, - { id: "z-ai/glm-5.1", name: "Glm 5.1" }, + { id: "moonshotai/kimi-k2.7-code", name: "Kimi K2.7 Code" }, + { id: "moonshotai/kimi-k3-free", name: "Kimi K3 Free" }, + { id: "z-ai/glm-5.3-free", name: "Glm 5.3 Free" }, { id: "z-ai/glm-5.2", name: "Glm 5.2" }, + { id: "z-ai/glm-5-turbo", name: "Glm 5 Turbo" }, + { id: "x-ai/grok-4.5", name: "Grok 4.5" }, ], serviceKinds: ["llm", "embedding", "image"], embeddingConfig: { diff --git a/open-sse/providers/shared.js b/open-sse/providers/shared.js index fdd76c40..88488499 100644 --- a/open-sse/providers/shared.js +++ b/open-sse/providers/shared.js @@ -22,6 +22,7 @@ export function mapStainlessArch() { // Anthropic API version (single source — reused across claude-format providers/executors) export const ANTHROPIC_API_VERSION = "2023-06-01"; +export const CLAUDE_CLI_VERSION = "2.1.258"; // Shared Claude-compatible API headers (reused across claude-format providers) export const CLAUDE_API_HEADERS = { @@ -34,7 +35,7 @@ export const CLAUDE_CLI_SPOOF_HEADERS = { "Anthropic-Version": ANTHROPIC_API_VERSION, "Anthropic-Beta": "claude-code-20250219,oauth-2025-04-20,interleaved-thinking-2025-05-14,context-management-2025-06-27,prompt-caching-scope-2026-01-05,advanced-tool-use-2025-11-20,effort-2025-11-24,structured-outputs-2025-12-15,fast-mode-2026-02-01,redact-thinking-2026-02-12,token-efficient-tools-2026-03-28", "Anthropic-Dangerous-Direct-Browser-Access": "true", - "User-Agent": "claude-cli/2.1.92 (external, sdk-cli)", + "User-Agent": `claude-cli/${CLAUDE_CLI_VERSION} (external, sdk-cli)`, "X-App": "cli", "X-Stainless-Helper-Method": "stream", "X-Stainless-Retry-Count": "0", @@ -74,10 +75,10 @@ export const KIMI_CODING_BASE_URL = "https://api.kimi.com/coding/v1/messages"; export const OPENAI_COMPAT_BASE = "https://api.openai.com/v1"; export const ANTHROPIC_COMPAT_BASE = "https://api.anthropic.com/v1"; -// Official Antigravity IDE Desktop 2.1.1 fingerprint captured from macOS arm64. +// Official Antigravity IDE Desktop 2.11.0 fingerprint captured from macOS arm64. // Keep this static even when 9router runs on Linux: the provider profile is // intentionally matching the IDE client, not the server host. -export const ANTIGRAVITY_IDE_VERSION = "2.1.1"; +export const ANTIGRAVITY_IDE_VERSION = "2.11.0"; export const ANTIGRAVITY_IDE_BASE_URL = "https://daily-cloudcode-pa.googleapis.com"; export const ANTIGRAVITY_IDE_USER_AGENT = `antigravity/ide/${ANTIGRAVITY_IDE_VERSION} darwin/arm64`; diff --git a/open-sse/providers/thinkingLevels.js b/open-sse/providers/thinkingLevels.js index f42cdd84..b56896b1 100644 --- a/open-sse/providers/thinkingLevels.js +++ b/open-sse/providers/thinkingLevels.js @@ -39,6 +39,13 @@ const PATTERN_THINKING = [ { provider: "codex", pattern: "*gpt-5.6-terra*", levels: [...CODEX_GPT_5_6_LEVELS, "ultra"] }, { provider: "codex", pattern: "*gpt-5.6-luna*", levels: CODEX_GPT_5_6_LEVELS }, { pattern: "*codex*", levels: ["low", "medium", "high", "xhigh"] }, // codex cannot disable thinking + // codebuddy-cn per-model effort sets — read off the client picker (server- + // delivered supportedEfforts), 2026-08-30. Gateway uses thinkingFormat "openai" + // but rejects levels outside each model's set. + { provider: "codebuddy-cn", pattern: "glm-5.3*", levels: ["low", "high", "max"] }, + { provider: "codebuddy-cn", pattern: "deepseek-v4*", levels: ["low", "high", "xhigh"] }, + { provider: "codebuddy-cn", pattern: "hy3*", levels: ["low", "high"] }, + { provider: "codebuddy-cn", pattern: "hy4*", levels: ["high"] }, ]; // Returns valid thinking levels for a model, or null when the model has no reasoning. diff --git a/open-sse/services/usage.js b/open-sse/services/usage.js index 5d39547a..a2ed59b1 100644 --- a/open-sse/services/usage.js +++ b/open-sse/services/usage.js @@ -15,6 +15,7 @@ import { getGrokCliUsage } from "./usage/grok-cli.js"; import { getKimiUsage } from "./usage/kimi.js"; import { getDeepseekUsage } from "./usage/deepseek.js"; import { getFreebuffUsage } from "./usage/freebuff.js"; +import { getGroqUsage } from "./usage/groq.js"; import { getZedUsage } from "./usage/zed.js"; import { resolveQoderCredentials } from "./qoderModels.js"; import { getGlmUsage } from "./usage/glm.js"; @@ -57,6 +58,7 @@ const USAGE_HANDLERS = { kimi: (c) => getKimiUsage(c.accessToken, c.apiKey, c.proxyOptions, c.providerSpecificData), deepseek: (c) => getDeepseekUsage(c.apiKey, c.proxyOptions), freebuff: (c) => getFreebuffUsage(c.accessToken, c.providerSpecificData, c.proxyOptions), + groq: (c) => getGroqUsage(c.apiKey, c.proxyOptions), zed: (c) => getZedUsage(c.accessToken, c.providerSpecificData, c.proxyOptions), }; diff --git a/open-sse/services/usage/google.js b/open-sse/services/usage/google.js index c7564c00..9afbe4ca 100644 --- a/open-sse/services/usage/google.js +++ b/open-sse/services/usage/google.js @@ -161,6 +161,9 @@ export async function getAntigravityUsage(accessToken, providerSpecificData, pro if (data.models) { // Filter only recommended/important models (must match PROVIDER_MODELS ag ids) const importantModels = [ + 'gemini-3.8-flash-high', + 'gemini-3.8-flash-medium', + 'gemini-3.8-flash-low', 'gemini-3.7-flash-high', 'gemini-3.7-flash-medium', 'gemini-3.7-flash-low', diff --git a/open-sse/services/usage/groq.js b/open-sse/services/usage/groq.js new file mode 100644 index 00000000..61be0892 --- /dev/null +++ b/open-sse/services/usage/groq.js @@ -0,0 +1,133 @@ +/** + * Groq usage — no dedicated quota endpoint. Rate-limit info instead rides on + * every API response as x-ratelimit-* headers (requests + tokens, always + * included). We piggyback on the models list (already used as + * transport.validateUrl) so reading usage never costs tokens. + * + * Headers: + * x-ratelimit-limit-requests / x-ratelimit-remaining-requests + * x-ratelimit-limit-tokens / x-ratelimit-remaining-tokens + * x-ratelimit-reset-requests / x-ratelimit-reset-tokens (duration strings, e.g. "2m59.56s") + * + * Docs: https://console.groq.com/docs/rate-limits + */ + +import { proxyAwareFetch } from "../../utils/proxyFetch.js"; +import { U } from "./shared.js"; + +const MODELS_URL = U("groq").url; + +// Groq reset headers are Go-style duration strings ("2m59.56s", "7.66s"), not +// timestamps — parse the h/m/s/ms components and add them to now(). +function parseGroqDurationMs(value) { + if (typeof value !== "string" || !value.trim()) return null; + + const re = /(\d+(?:\.\d+)?)(ms|s|m|h)/g; + let match; + let totalMs = 0; + let matched = false; + while ((match = re.exec(value))) { + matched = true; + const amount = Number(match[1]); + const unit = match[2]; + const unitMs = unit === "h" ? 3600000 : unit === "m" ? 60000 : unit === "ms" ? 1 : 1000; + totalMs += amount * unitMs; + } + return matched ? totalMs : null; +} + +function resetAtFromDuration(value) { + const ms = parseGroqDurationMs(value); + return ms === null ? null : new Date(Date.now() + ms).toISOString(); +} + +function buildRateLimitQuota(headers, limitKey, remainingKey, resetKey) { + // headers.get() returns null when absent, and Number(null) is 0 (a finite + // number) — check presence explicitly so a missing header can't masquerade + // as a real "0 remaining" quota. + const limitRaw = headers.get(limitKey); + const remainingRaw = headers.get(remainingKey); + if (limitRaw === null || remainingRaw === null) return null; + + const limit = Number(limitRaw); + const remaining = Number(remainingRaw); + if (!Number.isFinite(limit) || !Number.isFinite(remaining)) return null; + + return { + used: Math.max(0, limit - remaining), + total: limit, + resetAt: resetAtFromDuration(headers.get(resetKey)), + unlimited: false, + }; +} + +/** + * @param {string|null|undefined} apiKey + * @param {object|null} proxyOptions + */ +export async function getGroqUsage(apiKey, proxyOptions = null) { + if (!apiKey || typeof apiKey !== "string" || !apiKey.trim()) { + return { message: "Groq API key not available. Add a key to view usage." }; + } + + try { + const response = await proxyAwareFetch( + MODELS_URL, + { + method: "GET", + headers: { + Authorization: `Bearer ${apiKey.trim()}`, + Accept: "application/json", + }, + }, + proxyOptions, + ); + + if (response.status === 401 || response.status === 403) { + return { plan: "Groq", message: "Groq authentication failed. Check the API key." }; + } + + if (!response.ok) { + const errText = await response.text().catch(() => ""); + return { + plan: "Groq", + message: `Groq usage API error (${response.status})${errText ? `: ${errText.slice(0, 120)}` : ""}`, + }; + } + + // The quota data lives in headers, not the body — drain it so the + // connection can be released without needing the payload. + await response.text().catch(() => {}); + + const requests = buildRateLimitQuota( + response.headers, + "x-ratelimit-limit-requests", + "x-ratelimit-remaining-requests", + "x-ratelimit-reset-requests", + ); + const tokens = buildRateLimitQuota( + response.headers, + "x-ratelimit-limit-tokens", + "x-ratelimit-remaining-tokens", + "x-ratelimit-reset-tokens", + ); + + if (!requests && !tokens) { + // Key is valid (request succeeded) but no rate-limit bucket reported — + // distinguish "not tracked yet" from an auth/error state. + return { + plan: "Groq", + message: "Groq connected. No rate-limit data reported for this key yet.", + quotas: {}, + }; + } + + const quotas = {}; + if (requests) quotas["Requests"] = requests; + if (tokens) quotas["Tokens"] = tokens; + + return { plan: "Groq", quotas }; + } catch (error) { + return { message: `Groq error: ${error.message}` }; + } +} diff --git a/open-sse/translator/concerns/thinkingUnified.js b/open-sse/translator/concerns/thinkingUnified.js index 9902c314..a3035823 100644 --- a/open-sse/translator/concerns/thinkingUnified.js +++ b/open-sse/translator/concerns/thinkingUnified.js @@ -105,12 +105,16 @@ export function extractThinking(body) { // at the call-site where intent is snapshotted before format translation. export const captureThinking = extractThinking; -// Resolve thinking format: provider override > capability > derive(targetFormat). +const NATIVE_ONLY_FORMATS = new Set(["gemini-level", "gemini-budget", "claude-budget", "claude-adaptive", "kiro"]); + function resolveFormat(targetFormat, model, provider) { const providerFmt = provider ? PROVIDERS[provider]?.thinkingFormat : null; if (providerFmt) return providerFmt; const caps = getCapabilitiesForModel(provider, model); - if (caps.thinkingFormat) return caps.thinkingFormat; + const isOpenAIWire = targetFormat === "openai" || targetFormat === "openai-responses"; + if (caps.thinkingFormat && !(isOpenAIWire && NATIVE_ONLY_FORMATS.has(caps.thinkingFormat))) { + return caps.thinkingFormat; + } return FORMAT_TO_NATIVE[targetFormat] || "openai"; } @@ -237,12 +241,10 @@ function applyFormat(fmt, body, cfg, caps, supportedLevels) { } case "claude-adaptive": { if (none && canDisable) { body.thinking = { type: "disabled" }; break; } - // output_config.effort alone does NOT turn thinking on: Anthropic requires - // an explicit thinking:{type:"adaptive"} on Opus 4.6/4.7/4.8 and Sonnet 4.6 - // ("thinking is off unless you explicitly set it"), and Anthropic-compatible - // shims (e.g. GitHub Copilot /v1/messages) default thinking off even for - // Sonnet 5. Send both fields — the documented adaptive-thinking shape. - body.thinking = { type: "adaptive" }; + // Models that can disable thinking need the explicit adaptive switch. + // Permanently adaptive models such as Fable 5.1 accept effort directly. + if (canDisable) body.thinking = { type: "adaptive" }; + else delete body.thinking; const level = toLevel(eff); body.output_config = { effort: level === "xhigh" ? "high" : level }; break; diff --git a/open-sse/translator/formats/claude.js b/open-sse/translator/formats/claude.js index 81e72a44..62a0531a 100644 --- a/open-sse/translator/formats/claude.js +++ b/open-sse/translator/formats/claude.js @@ -12,6 +12,18 @@ import { DEFAULT_MAX_TOKENS } from "../../config/runtimeConfig.js"; const CACHE_CONTROL_5M = { type: "ephemeral" }; const CACHE_CONTROL_1H = { type: "ephemeral", ttl: "1h" }; +// Anthropic rejects a tool carrying BOTH defer_loading:true and cache_control +// ("Tools defer_loading cannot use prompt caching", #3567). MCP clients put +// deferred tools at the tail, which is exactly where the cache anchor lands. +// Anchor on the last tool that CAN be cached instead of dropping caching. +export function lastCacheableToolIndex(tools) { + if (!Array.isArray(tools)) return -1; + for (let i = tools.length - 1; i >= 0; i--) { + if (tools[i]?.defer_loading !== true) return i; + } + return -1; +} + // Check if message has valid non-empty content export function hasValidContent(msg) { if (typeof msg.content === "string" && msg.content.trim()) return true; @@ -108,11 +120,24 @@ function buildThinkingPlaceholder(provider) { return block; } +// Anthropic validates server_tool_use ids against this pattern and rejects the +// whole request with a 400 when one does not match. A combo that falls back to a +// provider with its own built-in tools (z.ai/glm emits OpenAI-style `call_` ids for +// its analyze_image tool) leaves such blocks in the history, so every later Claude +// turn carries a poisoned id. +const CLAUDE_SERVER_TOOL_USE_ID = /^srvtoolu_[a-zA-Z0-9_]+$/; + +function hasForeignServerToolUseId(block) { + return block?.type === CLAUDE_BLOCK.SERVER_TOOL_USE + && !CLAUDE_SERVER_TOOL_USE_ID.test(String(block.id ?? "")); +} + // Normalize a native Claude passthrough body to match Anthropic Messages API spec. // Newer Cowork/Claude Code clients emit beta-only shapes that OAuth endpoints reject: // 1. thinking.type "adaptive" → unsupported on Haiku // 2. output_config.effort → unsupported on Haiku // 3. role "system" messages (mid-conversation-system beta) → only top-level system is allowed +// 4. server_tool_use blocks carrying a foreign (non-srvtoolu_) id → rejected outright export function normalizeClaudePassthrough(body, model = "") { if (!body || typeof body !== "object") return body; @@ -164,6 +189,7 @@ export function normalizeClaudePassthrough(body, model = "") { // 3. Drop thinking blocks whose signature is not Claude's (combo mixes models, // so foreign signatures leak into history and Anthropic rejects them). const thinkingEnabled = body.thinking?.type === "enabled"; + const droppedServerToolUseIds = new Set(); if (Array.isArray(body.messages)) { for (const msg of body.messages) { if (msg.role !== ROLE.ASSISTANT || !Array.isArray(msg.content)) continue; @@ -178,6 +204,10 @@ export function normalizeClaudePassthrough(body, model = "") { } continue; } + if (hasForeignServerToolUseId(block)) { + if (block.id != null) droppedServerToolUseIds.add(String(block.id)); + continue; + } if (block.type === CLAUDE_BLOCK.TOOL_USE) hasToolUse = true; kept.push(block); } @@ -188,6 +218,35 @@ export function normalizeClaudePassthrough(body, model = "") { } } + // A dropped server_tool_use leaves its result behind; Anthropic rejects a + // tool_result that references an id no block declares, so both halves must go. + if (droppedServerToolUseIds.size > 0 && Array.isArray(body.messages)) { + for (const msg of body.messages) { + if (!Array.isArray(msg.content)) continue; + const kept = msg.content.filter(block => !( + (block?.type === CLAUDE_BLOCK.TOOL_RESULT || block?.type === CLAUDE_BLOCK.WEB_SEARCH_TOOL_RESULT) + && droppedServerToolUseIds.has(String(block.tool_use_id ?? "")) + )); + if (kept.length !== msg.content.length) { + msg.content = kept; + } + } + } + + // 5. Drop empty text blocks and any message left with no content at all. + // Anthropic rejects `messages.N.content` blocks with empty text (400 + // "text content blocks must be non-empty"); a message whose blocks were all + // stripped above must be dropped, not padded with an empty placeholder. + if (Array.isArray(body.messages)) { + body.messages = body.messages.filter(msg => { + if (typeof msg.content === "string") return msg.content.trim().length > 0; + if (!Array.isArray(msg.content)) return true; + msg.content = msg.content.filter(block => + !(block?.type === CLAUDE_BLOCK.TEXT && !String(block.text ?? "").trim())); + return msg.content.length > 0; + }); + } + return body; } @@ -223,7 +282,7 @@ export function anchorClaudeCache(body) { } if (Array.isArray(body.tools)) { - const last = body.tools.length - 1; + const last = lastCacheableToolIndex(body.tools); body.tools.forEach((tool, i) => { if (i === last) tool.cache_control = { ...CACHE_CONTROL_1H }; else delete tool.cache_control; @@ -417,9 +476,10 @@ export function prepareClaudeRequest(body, provider = null, apiKey = null, conne }); } + const lastCacheable = lastCacheableToolIndex(body.tools); body.tools = body.tools.map((tool, i) => { const { cache_control, ...rest } = tool; - if (i === body.tools.length - 1) { + if (i === lastCacheable) { return { ...rest, cache_control: { type: "ephemeral", ttl: "1h" } }; } return rest; diff --git a/open-sse/translator/formats/gemini.js b/open-sse/translator/formats/gemini.js index 6393a78b..8a965fee 100644 --- a/open-sse/translator/formats/gemini.js +++ b/open-sse/translator/formats/gemini.js @@ -14,6 +14,8 @@ export const UNSUPPORTED_SCHEMA_CONSTRAINTS = [ "uniqueItems", "contains", // 2020-12 keywords with no Gemini equivalent "unevaluatedProperties", "unevaluatedItems", "contentSchema", + // Tuple-array keywords; converted to items first, leftovers stripped + "prefixItems", "additionalItems", // Claude rejects these in VALIDATED mode "default", "examples", // JSON Schema meta keywords @@ -308,6 +310,37 @@ function ensureObjectType(obj) { for (const v of Object.values(obj)) if (v && typeof v === "object") ensureObjectType(v); } +// Convert prefixItems (tuple validation) to items — Gemini cannot express tuples, +// and a type:"array" schema without items is rejected with "missing field" +function convertPrefixItems(obj) { + if (!obj || typeof obj !== "object") return; + + if (Array.isArray(obj.prefixItems) && obj.prefixItems.length > 0) { + const variants = obj.prefixItems.filter(s => s && s.type !== "null"); + if (!obj.items && variants.length === 1) { + obj.items = variants[0]; + } else if (!obj.items && variants.length > 1) { + obj.items = { anyOf: variants }; + } + delete obj.prefixItems; + } + + for (const value of Object.values(obj)) { + if (value && typeof value === "object") { + convertPrefixItems(value); + } + } +} + +// Gemini requires items on every type:"array" schema — fill a permissive placeholder +function ensureArrayItems(obj) { + if (!obj || typeof obj !== "object") return; + if (obj.type === "array" && !obj.items) { + obj.items = { type: "string" }; + } + for (const v of Object.values(obj)) if (v && typeof v === "object") ensureArrayItems(v); +} + // Clean JSON Schema for Antigravity API compatibility - removes unsupported keywords recursively export function cleanJSONSchemaForAntigravity(schema) { if (!schema || typeof schema !== "object") return schema; @@ -321,11 +354,13 @@ export function cleanJSONSchemaForAntigravity(schema) { // Phase 2: Flatten complex structures mergeAllOf(cleaned); + convertPrefixItems(cleaned); flattenAnyOfOneOf(cleaned); flattenTypeArrays(cleaned); // Phase 2.5: Infer missing type=object when properties exist (Gemini requirement) ensureObjectType(cleaned); + ensureArrayItems(cleaned); // Phase 3: Remove all unsupported keywords at ALL levels (including inside arrays) removeUnsupportedKeywords(cleaned, UNSUPPORTED_SCHEMA_CONSTRAINTS); diff --git a/open-sse/translator/request/claude-to-kiro.js b/open-sse/translator/request/claude-to-kiro.js index 972b0610..3cfad109 100644 --- a/open-sse/translator/request/claude-to-kiro.js +++ b/open-sse/translator/request/claude-to-kiro.js @@ -327,7 +327,6 @@ export function claudeToKiroRequest(model, body, stream, credentials) { }; if (profileArn) payload.profileArn = profileArn; - if (systemPrompt) payload.systemPrompt = systemPrompt; if (additionalModelRequestFields) { payload.additionalModelRequestFields = additionalModelRequestFields; } diff --git a/open-sse/translator/request/openai-to-kiro.js b/open-sse/translator/request/openai-to-kiro.js index 3e30d2f5..1d9bedec 100644 --- a/open-sse/translator/request/openai-to-kiro.js +++ b/open-sse/translator/request/openai-to-kiro.js @@ -420,7 +420,6 @@ export function openaiToKiroRequest(model, body, stream, credentials) { if (profileArn) { payload.profileArn = profileArn; } - if (systemPrompt) payload.systemPrompt = systemPrompt; if (additionalModelRequestFields) { payload.additionalModelRequestFields = additionalModelRequestFields; } diff --git a/open-sse/translator/schema/blocks.js b/open-sse/translator/schema/blocks.js index 958122ce..91c71c4a 100644 --- a/open-sse/translator/schema/blocks.js +++ b/open-sse/translator/schema/blocks.js @@ -20,6 +20,8 @@ export const CLAUDE_BLOCK = { TOOL_RESULT: "tool_result", THINKING: "thinking", REDACTED_THINKING: "redacted_thinking", + SERVER_TOOL_USE: "server_tool_use", + WEB_SEARCH_TOOL_RESULT: "web_search_tool_result", }; // OpenAI Responses API item types. diff --git a/open-sse/utils/claudeCloaking.js b/open-sse/utils/claudeCloaking.js index 4f43a670..7e3790df 100644 --- a/open-sse/utils/claudeCloaking.js +++ b/open-sse/utils/claudeCloaking.js @@ -1,16 +1,16 @@ import { createHash, randomBytes, randomUUID } from "crypto"; import { CLAUDE_TOOL_SUFFIX, CC_DEFAULT_TOOLS } from "../config/appConstants.js"; +import { CLAUDE_CLI_VERSION } from "../providers/shared.js"; -const CLAUDE_VERSION = "2.1.92"; const CC_ENTRYPOINT = "sdk-cli"; -// Generate billing header matching real Claude Code 2.1.92+ format: +// Generate the billing header expected from current Claude Code clients. // x-anthropic-billing-header: cc_version=.; cc_entrypoint=sdk-cli; cch=; function generateBillingHeader(payload) { const content = JSON.stringify(payload); const cch = createHash("sha256").update(content).digest("hex").slice(0, 5); const buildHash = randomBytes(2).toString("hex").slice(0, 3); - return `x-anthropic-billing-header: cc_version=${CLAUDE_VERSION}.${buildHash}; cc_entrypoint=${CC_ENTRYPOINT}; cch=${cch};`; + return `x-anthropic-billing-header: cc_version=${CLAUDE_CLI_VERSION}.${buildHash}; cc_entrypoint=${CC_ENTRYPOINT}; cch=${cch};`; } // Derive a deterministic UUID-v4-shaped string from a seed (stable per account) @@ -19,7 +19,7 @@ function deriveUuid(seed) { return `${h.slice(0, 8)}-${h.slice(8, 12)}-4${h.slice(13, 16)}-${((parseInt(h[16], 16) & 0x3) | 0x8).toString(16)}${h.slice(17, 20)}-${h.slice(20, 32)}`; } -// Generate fake user ID in Claude Code 2.1.92+ JSON format: +// Generate fake user ID in the current Claude Code JSON format: // {"device_id":"<64hex>","account_uuid":"","session_id":""} // device_id/account_uuid derive from apiKey (stable per account), session_id per-conversation function generateFakeUserID(sessionId, apiKey) { diff --git a/open-sse/utils/modelMarkers.js b/open-sse/utils/modelMarkers.js new file mode 100644 index 00000000..6429fca0 --- /dev/null +++ b/open-sse/utils/modelMarkers.js @@ -0,0 +1,20 @@ +// Claude Code appends a bracketed context marker to the model name when the +// 1M-context beta is toggled on: `claude-opus-5` becomes `claude-opus-5[1m]`. +// The marker is a client-side annotation, not part of any model id: it never +// matches a combo name, an alias or a `provider/model` pair, so a request that +// carries it dies at model resolution with "Invalid model format". +// +// The capability itself travels in the `anthropic-beta: context-1m-2025-08-07` +// header, which is forwarded untouched — stripping the marker is enough to let +// the request route normally and still reach the upstream as a 1M request. + +const CONTEXT_MARKER = /\[1m\]$/i; + +// Returns { model, contextMarker } — contextMarker is null when there is none. +export function stripModelContextMarker(modelStr) { + if (typeof modelStr !== "string") return { model: modelStr, contextMarker: null }; + const trimmed = modelStr.trim(); + const match = trimmed.match(CONTEXT_MARKER); + if (!match) return { model: modelStr, contextMarker: null }; + return { model: trimmed.slice(0, -match[0].length), contextMarker: match[0].slice(1, -1).toLowerCase() }; +} diff --git a/package.json b/package.json index b36fa38c..b0d95e11 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "9router-app", - "version": "0.5.59", + "version": "0.5.65", "description": "9Router web dashboard", "private": true, "scripts": { diff --git a/public/i18n/literals/id.json b/public/i18n/literals/id.json index c60c1397..cdc5c77e 100644 --- a/public/i18n/literals/id.json +++ b/public/i18n/literals/id.json @@ -1,197 +1,1393 @@ { - "Cancel": "Batalkan", - "Delete": "Hapus", - "Edit": "Sunting", - "Save": "Simpan", - "Close": "Tutup", - "Add": "Tambah", - "Remove": "Hapus", - "Settings": "Pengaturan", - "Profile": "Profil", - "Dashboard": "Dasbor", - "Logout": "Keluar", - "Login": "Masuk", - "Providers": "Penyedia", - "Style": "Gaya", - "Usage": "Statistik Penggunaan", + "($/1M tokens). Example: An input rate of 2.50 means $2.50 per 1,000,000 input tokens.": "($/1M token). Contoh: Tarif masukan 2.50 berarti $2.50 per 1.000.000 token masukan.", + "($/1M tokens). Example: Input rate of 2.50 means $2.50 per 1,000,000 input tokens.": "($/1M token). Contoh: Tarif masukan 2.50 berarti $2.50 per 1.000.000 token masukan.", + "(Caveman)": "(Caveman)", + "(Headroom)": "(Headroom)", + "(Ponytail)": "(Ponytail)", + "(RTK)": "(RTK)", + "(via inference test)": "(melalui uji inferensi)", + "+ Browse": "+ Telusuri", + "+ Combo": "+ Kombinasi", + "+ Custom": "+ Kustom", + "+ Save current as...": "+ Simpan yang saat ini sebagai...", + "-compatible models manually or import them from the /models endpoint.": "- model kompatibel secara manual atau impor dari titik akhir /models.", + ". Click \"Apply\" to auto-configure.": ". Klik \"Terapkan\" untuk konfigurasi otomatis.", + "1. CLI & SDKs": "1. CLI & SDK", + "1. Client Request (Input)": "1. Permintaan Klien (Masukan)", + "1. Generates SSL cert & adds to system keychain": "1. Membuat sertifikat SSL & menambahkan ke keychain sistem", + "2. 9Router Hub": "2. Hub 9Router", + "2. Provider Request (Translated)": "2. Permintaan Penyedia (Diterjemahkan)", + "2. Redirects": "2. Pengalihan", + "24h": "24 jam", + "3. AI Providers": "3. Penyedia AI", + "3. Maps Antigravity models to any provider via 9Router": "3. Memetakan model Antigravity ke penyedia mana pun melalui 9Router", + "3. Provider Response (Raw)": "3. Respons Penyedia (Mentah)", + "30D": "30 hari", + "4. Client Response (Final)": "4. Respons Klien (Final)", + "60D": "60 hari", + "7D": "7 hari", + "9Router (Entry)": "9Router (Masukan)", + "9Router Base URL": "URL Dasar 9Router", + ": Account | Workers Scripts | Edit": ": Akun | Skrip Workers | Edit", + ": Include | Account |": ": Sertakan | Akun |", + "AI endpoint proxy with web dashboard - A JavaScript port of CLIProxyAPI. Works seamlessly with Claude Code, OpenAI Codex, Cline, RooCode, and other CLI tools.": "Proxy titik akhir AI dengan dasbor web — Port JavaScript dari CLIProxyAPI. Berfungsi mulus dengan Claude Code, OpenAI Codex, Cline, RooCode, dan alat CLI lainnya.", + "API Endpoint": "Titik Akhir API", "API Key": "Kunci API", - "Connected": "Terhubung", - "Disconnected": "Terputus", - "Active": "Aktif", - "Inactive": "Nonaktif", - "Success": "Berhasil", - "Failed": "Gagal", - "Error": "Kesalahan", - "Warning": "Peringatan", - "Info": "Informasi", - "Loading": "Memuat", - "Search": "Cari", - "Filter": "Saring", - "Sort": "Urutkan", - "Export": "Ekspor", - "Import": "Impor", - "Refresh": "Segarkan", - "Back": "Kembali", - "Next": "Berikutnya", - "Previous": "Sebelumnya", - "Submit": "Kirim", - "Confirm": "Konfirmasi", - "Yes": "Ya", - "No": "Tidak", - "OK": "OK", - "Apply": "Terapkan", - "Reset": "Atur Ulang", - "Clear": "Hapus", - "Select": "Pilih", - "Upload": "Unggah", - "Download": "Unduh", - "Copy": "Salin", - "Paste": "Tempel", - "Cut": "Potong", - "Undo": "Batalkan", - "Redo": "Ulangi", - "Name": "Nama", - "Description": "Deskripsi", - "Status": "Status", - "Type": "Jenis", - "Date": "Tanggal", - "Time": "Waktu", - "Created": "Dibuat", - "Updated": "Diperbarui", - "Actions": "Tindakan", - "Details": "Detail", - "View": "Lihat", - "New": "Baru", - "Total": "Total", - "Count": "Jumlah", - "Price": "Harga", - "Cost": "Biaya", - "Free": "Gratis", - "Paid": "Berbayar", - "Enable": "Aktifkan", - "Disable": "Nonaktifkan", - "Enabled": "Diaktifkan", - "Disabled": "Dinonaktifkan", - "Online": "Daring", - "Offline": "Luring", - "Available": "Tersedia", - "Unavailable": "Tidak Tersedia", - "Required": "Diperlukan", - "Optional": "Opsional", - "Default": "Bawaan", - "Custom": "Kustom", - "Advanced": "Lanjutan", - "Basic": "Dasar", - "Help": "Bantuan", - "Support": "Dukungan", - "Documentation": "Dokumentasi", - "Version": "Versi", - "Language": "Bahasa", - "Theme": "Tema", - "Light": "Terang", - "Dark": "Gelap", - "Auto": "Otomatis", - "Endpoint": "Titik Akhir", - "Combos": "Kombinasi", - "Quota Tracker": "Pelacak Kuota", - "MITM": "MITM", - "CLI Tools": "Alat", - "Console Log": "Log Konsol", - "System": "Sistem", - "Debug": "Debug", - "Shutdown": "Matikan", - "Close Proxy": "Tutup Proxy", - "Are you sure you want to close the proxy server?": "Apakah Anda yakin ingin menutup server proxy?", - "Server Disconnected": "Server Terputus", - "The proxy server has been stopped.": "Server proxy telah dihentikan.", - "Reload Page": "Muat Ulang Halaman", - "Service is running in terminal. You can close this web page. Shutdown will stop the service.": "Layanan sedang berjalan di terminal. Anda dapat menutup halaman web ini. Shutdown akan menghentikan layanan.", - "Manage your AI provider connections": "Kelola koneksi penyedia AI Anda", - "Model combos with fallback": "Kombinasi model dengan fallback", - "Monitor your API usage, token consumption, and request logs": "Pantau penggunaan API, konsumsi token, dan log permintaan Anda", - "Intercept CLI tool traffic and route through 9Router": "Intersep lalu lintas alat CLI dan rute melalui 9Router", - "Configure CLI tools": "Konfigurasi alat CLI", + "API Key (for Check)": "Kunci API (untuk Pemeriksaan)", + "API Key Compatible Providers": "Penyedia Kompatibel Kunci API", + "API Key Created": "Kunci API Dibuat", + "API Key Name": "Nama Kunci API", + "API Key Providers": "Penyedia Kunci API", + "API Keys": "Kunci API", + "API Reference": "Referensi API", + "API Token": "Token API", + "API Tokens": "Token API", + "API Type": "Jenis API", + "API Version": "Versi API", "API endpoint configuration": "Konfigurasi titik akhir API", - "Manage your preferences": "Kelola preferensi Anda", - "Debug translation flow between formats": "Debug alur terjemahan antara format", - "Live server console output": "Output konsol server langsung", - "Create model combos with fallback support": "Buat kombinasi model dengan dukungan fallback", - "Local Mode": "Mode Lokal", - "Running on your machine": "Berjalan di mesin Anda", - "Database Location": "Lokasi Database", - "Download Backup": "Unduh Cadangan", - "Import Backup": "Impor Cadangan", - "Database backup downloaded": "Cadangan database telah diunduh", - "Database imported successfully": "Database berhasil diimpor", - "Security": "Keamanan", - "Require login": "Memerlukan Login", - "When ON, dashboard requires password. When OFF, access without login.": "Ketika AKTIF, dasbor memerlukan kata sandi. Ketika NONAKTIF, akses tanpa login.", - "Current Password": "Kata Sandi Saat Ini", - "Enter current password": "Masukkan kata sandi saat ini", - "New Password": "Kata Sandi Baru", - "Enter new password": "Masukkan kata sandi baru", - "Confirm New Password": "Konfirmasi Kata Sandi Baru", - "Confirm new password": "Konfirmasi kata sandi baru", - "Update Password": "Perbarui Kata Sandi", - "Set Password": "Atur Kata Sandi", - "Password updated successfully": "Kata sandi berhasil diperbarui", - "Passwords do not match": "Kata sandi tidak cocok", - "Routing Strategy": "Strategi Rute", - "Round Robin": "Putaran Bulat", - "Cycle through accounts to distribute load": "Siklus melalui akun untuk mendistribusikan beban", - "Sticky Limit": "Batas Lengket", + "AWS Builder ID": "ID Pembangun AWS", + "AWS IAM Identity Center": "Pusat Identitas AWS IAM", + "AWS Region": "Wilayah AWS", + "AWS region for the key (default: us-east-1)": "Wilayah AWS untuk kunci (bawaan: us-east-1)", + "AWS region for your Identity Center (default: us-east-1)": "Wilayah AWS untuk Pusat Identitas Anda (bawaan: us-east-1)", + "About": "Tentang", + "Access Anywhere": "Akses Di Mana Saja", + "Access Token": "Token Akses", + "Access token will be auto-filled...": "Token akses akan diisi otomatis...", + "Access your terminal, desktop & files from anywhere": "Akses terminal, desktop, dan file Anda dari mana saja", + "Account": "Akun", + "Account ID": "ID Akun", + "Account Resources": "Sumber Daya Akun", + "Accounts per page": "Akun per halaman", + "Action": "Tindakan", + "Activate": "Aktifkan", + "Active": "Aktif", + "Active All": "Aktifkan Semua", + "Active:": "Aktif:", + "Add": "Tambah", + "Add API Key": "Tambah Kunci API", + "Add Anthropic Compatible": "Tambah Kompatibel Anthropic", + "Add Connection": "Tambah Koneksi", + "Add Custom Embedding": "Tambah Embedding Kustom", + "Add Custom MCP": "Tambah MCP Kustom", + "Add Custom Model": "Tambah Model Kustom", + "Add Model": "Tambah Model", + "Add Model Config": "Tambah Konfigurasi Model", + "Add Model for GitHub Copilot": "Tambah Model untuk GitHub Copilot", + "Add Model for OpenCode": "Tambah Model untuk OpenCode", + "Add Model to Combo": "Tambah Model ke Kombinasi", + "Add New Provider": "Tambah Penyedia Baru", + "Add OpenAI Compatible": "Tambah Kompatibel OpenAI", + "Add Provider": "Tambah Penyedia", + "Add Proxy Pool": "Tambah Pool Proxy", + "Add Shorthands": "Tambah Nama Pendek", + "Add a connection to enable importing models.": "Tambah koneksi untuk mengaktifkan impor model.", + "Add connection using browser cookie": "Tambah koneksi menggunakan cookie browser", + "Add model": "Tambah model", + "Add server": "Tambah server", + "Add the following configuration to your models array:": "Tambahkan konfigurasi berikut ke array models Anda:", + "Add your first connection to get started": "Tambah koneksi pertama Anda untuk memulai", + "Administrator required": "Memerlukan administrator", + "Administrator required — restart 9Router as Administrator to use MITM": "Memerlukan administrator — mulai ulang 9Router sebagai Administrator untuk menggunakan MITM", + "After authorization, copy the full URL from your browser address bar.": "Setelah otorisasi, salin URL lengkap dari bilah alamat browser Anda.", + "After authorization, copy the full URL from your browser.": "Setelah otorisasi, salin URL lengkap dari browser Anda.", + "After installation, run": "Setelah instalasi, jalankan", + "After login, you'll need to copy the callback URL from your browser and paste it back here.": "Setelah login, Anda perlu menyalin URL callback dari browser dan menempelkannya kembali di sini.", + "Alibaba Qwen Code CLI — supports OpenAI, Anthropic & Gemini providers via 9Router": "Alibaba Qwen Code CLI — mendukung penyedia OpenAI, Anthropic & Gemini melalui 9Router", + "All": "Semua", + "All AI Providers": "Semua Penyedia AI", + "All Providers": "Semua Penyedia", + "All models are responding normally.": "Semua model merespons normal.", + "All providers": "Semua penyedia", + "All rates are in": "Semua tarif dalam", + "All selected currently unbound": "Semua yang dipilih saat ini tidak terikat", + "Allow dashboard access via tunnel": "Izinkan akses dasbor melalui tunnel", + "Allow either password or OIDC.": "Izinkan kata sandi atau OIDC.", + "An error occurred": "Terjadi kesalahan", + "An error occurred. Please try again.": "Terjadi kesalahan. Silakan coba lagi.", + "Anthropic Claude Code CLI": "Anthropic Claude Code CLI", + "Anthropic Compatible (Prod)": "Kompatibel Anthropic (Produksi)", + "Anthropic Compatible Details": "Detail Kompatibel Anthropic", + "Antigravity/Copilot IDE request → DNS redirect to localhost:443 → MITM proxy intercepts → 9Router → response to Antigravity/Copilot": "Permintaan IDE Antigravity/Copilot → pengalihan DNS ke localhost:443 → proxy MITM mengintersep → 9Router → respons ke Antigravity/Copilot", + "Any model available in 9Router can be used — not just Qwen models. Select from Qwen, Claude, Gemini, GPT, and more.": "Model apa pun yang tersedia di 9Router dapat digunakan — tidak hanya model Qwen. Pilih dari Qwen, Claude, Gemini, GPT, dan lainnya.", + "App Name": "Nama Aplikasi", + "Apply": "Terapkan", + "Apply Proxy": "Terapkan Proxy", + "Applying...": "Menerapkan...", + "Are you sure you want to close the proxy server?": "Apakah Anda yakin ingin menutup server proxy?", + "Are you sure you want to disable the tunnel?": "Apakah Anda yakin ingin menonaktifkan tunnel?", + "Attempting to reconnect...": "Mencoba menghubungkan kembali...", + "Audio File": "File Audio", + "Auth Mode": "Mode Autentikasi", + "Authenticate": "Autentikasi", + "Authentication Method": "Metode Autentikasi", + "Authentication Successful": "Autentikasi Berhasil", + "Authentication Successful!": "Autentikasi Berhasil!", + "Authless": "Tanpa Autentikasi", + "Authorization Successful!": "Otorisasi Berhasil!", + "Authorize": "Otorisasi", + "Auto (by priority)": "Otomatis (berdasarkan prioritas)", + "Auto Refresh (3s)": "Penyegaran Otomatis (3d)", + "Auto-detect": "Deteksi otomatis", + "Auto-detecting token...": "Mendeteksi token secara otomatis...", + "Auto-detecting tokens...": "Mendeteksi token secara otomatis...", + "Auto-ping": "Ping otomatis", + "Auto-refresh": "Penyegaran otomatis", + "Auto:": "Otomatis:", + "Automatically switch between providers when limits are hit.": "Beralih secara otomatis antar penyedia saat batas tercapai.", + "Available": "Tersedia", + "Available Models": "Model Tersedia", + "Azure Endpoint": "Titik Akhir Azure", + "Azure OpenAI Configuration": "Konfigurasi Azure OpenAI", + "BXAuth=xxx; ...": "BXAuth=xxx; ...", + "Back": "Kembali", + "Back to CLI Tools": "Kembali ke Alat CLI", + "Back to Providers": "Kembali ke Penyedia", + "Base URL": "URL Dasar", + "Batch Import": "Impor Batch", + "Batch Import Proxies": "Impor Proxy Batch", + "Batch Size": "Ukuran Batch", + "Beautiful web dashboard for managing providers and monitoring usage.": "Dasbor web indah untuk mengelola penyedia dan memantau penggunaan.", + "Best quality, but costs the most": "Kualitas terbaik, tetapi biaya paling mahal", + "Bias the model toward minimal code: YAGNI, reuse stdlib, deletion over addition": "Membimbing model ke kode minimal: YAGNI, gunakan ulang stdlib, hapus daripada menambah", + "Binary File": "File Biner", + "Blog": "Blog", + "Both": "Keduanya", + "Browse & edit files": "Telusuri dan edit file", + "Browse MCP Marketplace": "Jelajahi Pasar MCP", + "Browse source, README, and examples.": "Telusuri kode sumber, README, dan contoh.", + "Browser Control (Browser MCP)": "Kontrol Browser (Browser MCP)", + "Bulk Add": "Tambah Massal", + "CLI Support": "Dukungan CLI", + "CLI Tools": "Alat CLI", + "CLI on the host →": "CLI di host →", + "CLIProxyAPI Auth JSON": "JSON Autentikasi CLIProxyAPI", + "Cache Creation": "Pembuatan Cache", + "Cache Creation:": "Pembuatan Cache:", + "Cached": "Tersimpan di cache", + "Cached Tokens": "Token Tersimpan di Cache", + "Cached Tokens:": "Token Tersimpan di Cache:", + "Cached input tokens (typically 50% of input rate)": "Token masukan tersimpan di cache (biasanya 50% dari tarif masukan)", + "Cached:": "Tersimpan di cache:", "Calls per account before switching": "Panggilan per akun sebelum beralih", - "Network": "Jaringan", - "Outbound Proxy": "Proxy Keluar", - "Enable proxy for OAuth + provider outbound requests.": "Aktifkan proxy untuk permintaan keluar OAuth + penyedia.", - "Proxy URL": "URL Proxy", - "Leave empty to inherit existing env proxy (if any).": "Biarkan kosong untuk mewarisi proxy env yang ada (jika ada).", - "No Proxy": "Tidak Ada Proxy", + "Calls per combo model before switching": "Panggilan per model kombinasi sebelum beralih", + "Cancel": "Batalkan", + "Capacity auto-switch": "Beralih kapasitas otomatis", + "Cert": "Sertifikat", + "Change Log": "Catatan Perubahan", + "Changelog": "Catatan Perubahan", + "Chat": "Obrolan", + "Chat / code-gen via OpenAI or Anthropic format with streaming.": "Obrolan / pembuatan kode melalui format OpenAI atau Anthropic dengan streaming.", + "Chat Completions": "Pelengkapan Obrolan", + "Check": "Periksa", + "Checking Claude CLI...": "Memeriksa Claude CLI...", + "Checking Claude Cowork...": "Memeriksa Claude Cowork...", + "Checking Cline...": "Memeriksa Cline...", + "Checking Codex CLI...": "Memeriksa Codex CLI...", + "Checking Copilot config...": "Memeriksa konfigurasi Copilot...", + "Checking DeepSeek TUI...": "Memeriksa DeepSeek TUI...", + "Checking Factory Droid CLI...": "Memeriksa Factory Droid CLI...", + "Checking Hermes Agent...": "Memeriksa Hermes Agent...", + "Checking Kilo Code...": "Memeriksa Kilo Code...", + "Checking Open Claw CLI...": "Memeriksa Open Claw CLI...", + "Checking OpenCode CLI...": "Memeriksa OpenCode CLI...", + "Checking jcode CLI...": "Memeriksa jcode CLI...", + "Checking...": "Memeriksa...", + "Choose API Provider → Ollama": "Pilih Penyedia API → Ollama", + "Choose how to authenticate with GitLab Duo:": "Pilih cara autentikasi dengan GitLab Duo:", + "Choose your authentication method:": "Pilih metode autentikasi Anda:", + "Claude": "Claude", + "Claude CLI - Manual Configuration": "Claude CLI - Konfigurasi Manual", + "Claude CLI not detected locally": "Claude CLI tidak terdeteksi secara lokal", + "Claude CLI not installed": "Claude CLI tidak terinstal", + "Claude Cowork - Manual Configuration": "Claude Cowork - Konfigurasi Manual", + "Claude Desktop (Cowork mode) not detected": "Claude Desktop (mode Cowork) tidak terdeteksi", + "Claude Desktop Cowork (third-party inference)": "Claude Desktop Cowork (inferensi pihak ketiga)", + "Clear": "Hapus", + "Clear (will use main model)": "Hapus (akan menggunakan model utama)", + "Clear Filters": "Hapus Filter", + "Clear search": "Hapus pencarian", + "Click": "Klik", + "Click \"View All Model\" → \"Add Custom Model\"": "Klik \"Lihat Semua Model\" → \"Tambah Model Kustom\"", + "Click a model to set/clear active": "Klik model untuk mengatur/menghapus status aktif", + "Click to add, click again to remove. Changes are saved automatically.": "Klik untuk menambah, klik lagi untuk menghapus. Perubahan disimpan secara otomatis.", + "Click to edit": "Klik untuk mengedit", + "Click to retry": "Klik untuk mencoba lagi", + "Client ID": "ID Klien", + "Client Request": "Permintaan Klien", + "Client Response": "Respons Klien", + "Client Secret": "Rahasia Klien", + "Cline - Manual Configuration": "Cline - Konfigurasi Manual", + "Cline AI Coding Assistant": "Asisten Pemrograman AI Cline", + "Cline not detected locally": "Cline tidak terdeteksi secara lokal", + "Close": "Tutup", + "Close Proxy": "Tutup Proxy", + "Close provider filter": "Tutup filter penyedia", + "Close reset credit expiry modal": "Tutup modal kedaluwarsa kredit reset", + "Close test results": "Tutup hasil pengujian", + "Closing in": "Tutup dalam", + "Cloud Sync": "Sinkronisasi Cloud", + "Cloudflare Relay": "Relay Cloudflare", + "Cloudflare Tunnel": "Tunnel Cloudflare", + "Cloudflare Workers AI": "Cloudflare Workers AI", + "Codex CLI - Manual Configuration": "Codex CLI - Konfigurasi Manual", + "Codex CLI not detected locally": "Codex CLI tidak terdeteksi secara lokal", + "Codex CLI not installed": "Codex CLI tidak terinstal", + "Codex Reset Credit Expiry": "Kedaluwarsa Kredit Reset Codex", + "Codex uses": "Codex menggunakan", + "Combo Name": "Nama Kombinasi", + "Combo Round Robin": "Putaran Bulat Kombinasi", + "Combo Sticky Limit": "Batas Lengket Kombinasi", + "Combos": "Kombinasi", + "Coming soon...": "Segera hadir...", "Comma-separated hostnames/domains to bypass the proxy.": "Nama host/domain yang dipisahkan koma untuk melewati proxy.", - "Test proxy URL": "Uji URL Proxy", - "Proxy settings applied": "Pengaturan proxy diterapkan", - "Proxy enabled": "Proxy diaktifkan", - "Proxy disabled": "Proxy dinonaktifkan", - "Proxy test OK": "Tes proxy OK", - "Proxy test failed": "Tes proxy gagal", - "Please enter a Proxy URL to test": "Masukkan URL Proxy untuk diuji", - "Observability": "Observabilitas", + "Comma-separated hosts/domains to bypass proxy": "Nama host/domain yang dipisahkan koma untuk melewati proxy", + "Company": "Perusahaan", + "Complete the authorization in the popup window.": "Selesaikan otorisasi di jendela popup.", + "Completion/response tokens": "Token pelengkapan/respons", + "Compress LLM output": "Kompresi keluaran LLM", + "Compress context": "Kompresi konteks", + "Compress prompts via /v1/compress before routing to the model": "Kompresi prompt melalui /v1/compress sebelum merutekan ke model", + "Compress tool output": "Kompresi keluaran alat", + "Compress tool output to reduce token usage.": "Kompresi keluaran alat untuk mengurangi penggunaan token.", + "Config path: Linux/macOS ~/.deepseek/config.toml • Windows %USERPROFILE%\\.deepseek\\config.toml": "Jalur konfigurasi: Linux/macOS ~/.deepseek/config.toml • Windows %USERPROFILE%\\.deepseek\\config.toml", + "Config path: Linux/macOS ~/.qwen/settings.json • Windows %USERPROFILE%\\.qwen\\settings.json": "Jalur konfigurasi: Linux/macOS ~/.qwen/settings.json • Windows %USERPROFILE%\\.qwen\\settings.json", + "Configuration": "Konfigurasi", + "Configure 9router as an OpenAI-compatible provider to route all jcode requests through 9router's optimization layer.": "Konfigurasi 9router sebagai penyedia kompatibel OpenAI untuk merutekan semua permintaan jcode melalui lapisan optimasi 9router.", + "Configure CLI tools": "Konfigurasi alat CLI", + "Configure a new AI provider to use with your applications.": "Konfigurasi penyedia AI baru untuk digunakan dengan aplikasi Anda.", + "Configure pricing rates for cost tracking and calculations": "Konfigurasi tarif penetapan harga untuk pelacakan dan perhitungan biaya", + "Configure providers and API keys via web interface": "Konfigurasi penyedia dan kunci API melalui antarmuka web", + "Configured": "Dikonfigurasi", + "Confirm": "Konfirmasi", + "Confirm New Password": "Konfirmasi Kata Sandi Baru", + "Confirm Password": "Konfirmasi Kata Sandi", + "Confirm new password": "Konfirmasi kata sandi baru", + "Connect": "Hubungkan", + "Connect AI tools remotely": "Hubungkan alat AI secara remote", + "Connect Cursor IDE": "Hubungkan Cursor IDE", + "Connect GitLab Duo": "Hubungkan GitLab Duo", + "Connect Kiro": "Hubungkan Kiro", + "Connect to providers with OAuth to track your API quota limits and usage.": "Hubungkan ke penyedia dengan OAuth untuk melacak batas kuota API dan penggunaan Anda.", + "Connect via OAuth or API keys. Securely manage credentials.": "Hubungkan melalui OAuth atau kunci API. Kelola kredensial dengan aman.", + "Connect with OAuth2": "Hubungkan dengan OAuth2", + "Connect your account using OAuth2 authentication.": "Hubungkan akun Anda menggunakan autentikasi OAuth2.", + "Connected": "Terhubung", + "Connected Successfully!": "Berhasil Terhubung!", + "Connected providers only": "Hanya penyedia yang terhubung", + "Connecting...": "Menghubungkan...", + "Connection": "Koneksi", + "Connection Details": "Detail Koneksi", + "Connection Failed": "Koneksi Gagal", + "Connections": "Koneksi", + "Console Log": "Log Konsol", + "Contact": "Kontak", + "Content": "Konten", + "Continue": "Lanjutkan", + "Continue AI Assistant": "Lanjutkan Asisten AI", + "Continue to summary": "Lanjutkan ke ringkasan", + "Continue with GitHub": "Lanjutkan dengan GitHub", + "Continue with Google": "Lanjutkan dengan Google", + "Cookie": "Cookie", + "Cookie Auth": "Autentikasi Cookie", + "Cookie String": "String Cookie", + "Cooldown": "Pendinginan", + "Copied!": "Disalin!", + "Copy": "Salin", + "Copy & Shutdown": "Salin & Matikan", + "Copy This URL": "Salin URL Ini", + "Copy a link and paste to your AI to use 9Router — no install needed": "Salin tautan dan tempel ke AI Anda untuk menggunakan 9Router — tidak perlu instalasi", + "Copy combo name": "Salin nama kombinasi", + "Copy install command": "Salin perintah instalasi", + "Copy model": "Salin model", + "Copy the JSON below to your ~/.qwen/settings.json file.": "Salin JSON di bawah ini ke file ~/.qwen/settings.json Anda.", + "Copy the entire cookie string (must include BXAuth)": "Salin seluruh string cookie (harus menyertakan BXAuth)", + "Cost": "Biaya", + "Cost Calculation:": "Perhitungan Biaya:", + "Costs": "Biaya", + "Costs are calculated based on token usage and pricing rates. Each request's cost is determined by: (input_tokens × input_rate) + (output_tokens × output_rate) + (cached_tokens × cached_rate)": "Biaya dihitung berdasarkan penggunaan token dan tarif penetapan harga. Biaya setiap permintaan ditentukan oleh: (input_tokens × input_rate) + (output_tokens × output_rate) + (cached_tokens × cached_rate)", + "Could not read Cursor database automatically.": "Tidak dapat membaca database Cursor secara otomatis.", + "Create": "Buat", + "Create API Key": "Buat Kunci API", + "Create Combo": "Buat Kombinasi", + "Create Cowork Combo": "Buat Kombinasi Cowork", + "Create Key": "Buat Kunci", + "Create Provider": "Buat Penyedia", + "Create Token": "Buat Token", + "Create a": "Buat", + "Create a proxy pool entry, then assign it to connections.": "Buat entri pool proxy, lalu tetapkan ke koneksi.", + "Create model combos with fallback support": "Buat kombinasi model dengan dukungan fallback", + "Create your first API key to get started": "Buat kunci API pertama Anda untuk memulai", + "Created": "Dibuat", + "Creating...": "Membuat...", + "Current": "Saat Ini", + "Current Password": "Kata Sandi Saat Ini", + "Current Pricing Overview": "Ringkasan Harga Saat Ini", + "Current password": "Kata sandi saat ini", + "Current: Keeps": "Saat Ini: Pertahankan", + "Currently using accounts in priority order (Fill First).": "Saat ini menggunakan akun dalam urutan prioritas (Isi Terlebih Dahulu).", + "Cursor AI Code Editor": "Editor Kode AI Cursor", + "Cursor IDE not detected. Please paste your tokens manually.": "Cursor IDE tidak terdeteksi. Silakan tempel token Anda secara manual.", + "Cursor routes requests through its own server, so local endpoint is not supported. Please enable Tunnel or Cloud Endpoint in Settings.": "Cursor merutekan permintaan melalui servernya sendiri, sehingga titik akhir lokal tidak didukung. Silakan aktifkan Tunnel atau Titik Akhir Cloud di Pengaturan.", + "Custom": "Kustom", + "Custom Pricing:": "Harga Kustom:", + "Custom Providers (OpenAI/Anthropic Compatible)": "Penyedia Kustom (Kompatibel OpenAI/Anthropic)", + "Custom Token": "Token Kustom", + "Custom accounts per page": "Kustom akun per halaman", + "Custom providers": "Penyedia kustom", + "Custom...": "Kustom...", + "Cycle through accounts to distribute load": "Siklus melalui akun untuk mendistribusikan beban", + "Cycle through providers in combos instead of always starting with first": "Siklus melalui penyedia dalam kombinasi alih-alih selalu mulai dari yang pertama", + "DNS off": "DNS mati", + "Dashboard": "Dasbor", + "Dashboard Password": "Kata Sandi Dasbor", + "Dashboard:": "Dasbor:", + "Data Location:": "Lokasi Data:", + "Data flows seamlessly from your application through our intelligent routing layer to the best provider for the job.": "Data mengalir mulus dari aplikasi Anda melalui lapisan rute cerdas kami ke penyedia terbaik untuk pekerjaan tersebut.", + "Data flows seamlessly through our intelligent routing system": "Data mengalir mulus melalui sistem rute cerdas kami", + "Database Location": "Lokasi Database", + "Database backup downloaded": "Cadangan database berhasil diunduh", + "Database imported successfully": "Database berhasil diimpor", + "DateTime": "Tanggal Waktu", + "Deactivate": "Nonaktifkan", + "Debug": "Debug", + "Debug translation flow between formats": "Debug alur terjemahan antara format", + "DeepSeek TUI - Manual Configuration": "DeepSeek TUI - Konfigurasi Manual", + "DeepSeek TUI not detected locally": "DeepSeek TUI tidak terdeteksi secara lokal", + "DeepSeek TUI uses ~/.deepseek/config.toml for configuration. 9Router will update the provider to 'openai' mode with your base_url, api_key, and model.": "DeepSeek TUI menggunakan ~/.deepseek/config.toml untuk konfigurasi. 9Router akan memperbarui penyedia ke mode 'openai' dengan base_url, api_key, dan model Anda.", + "DeepSeek Terminal Coding Agent (Rust TUI)": "Agen Pemrograman Terminal DeepSeek (Rust TUI)", + "Default Model": "Model Bawaan", + "Default password is": "Kata sandi bawaan adalah", + "Default password is 123456": "Kata sandi bawaan adalah 123456", + "Delete": "Hapus", + "Delete API Key": "Hapus Kunci API", + "Delete connection": "Hapus koneksi", + "Delete saved endpoint": "Hapus titik akhir yang disimpan", + "Delete selected preset": "Hapus preset yang dipilih", + "Delete this combo?": "Hapus kombinasi ini?", + "Delete this connection?": "Hapus koneksi ini?", + "Deno Deploy API Token": "Token API Deno Deploy", + "Deno Deploy v2 runs on a high-performance global edge network": "Deno Deploy v2 berjalan di jaringan edge global berkinerja tinggi", + "Deno Relay": "Relay Deno", + "Deploy": "Deploy", + "Deploy Cloudflare Relay": "Deploy Relay Cloudflare", + "Deploy Deno Relay": "Deploy Relay Deno", + "Deploy Relay": "Deploy Relay", + "Deploy Vercel Relay": "Deploy Relay Vercel", + "Deploy multiple relays for maximum IP diversity": "Deploy beberapa relay untuk keragaman IP maksimal", + "Deploy multiple relays on different accounts for more IP diversity": "Deploy beberapa relay di akun berbeda untuk keragaman IP lebih banyak", + "Deploying... (may take ~1 min)": "MendePLOY... (mungkin memakan waktu ~1 menit)", + "Deployment Name": "Nama Deployment", + "Deploys a Cloudflare Worker as a proxy relay. All AI provider requests will be forwarded through Cloudflare's global edge network.": "Mendeploy Cloudflare Worker sebagai relay proxy. Semua permintaan penyedia AI akan diteruskan melalui jaringan edge global Cloudflare.", + "Deploys a relay worker to Deno Deploy's global edge network. All AI provider requests are forwarded through Deno's edge, masking your real IP.": "Mendeploy relay worker ke jaringan edge global Deno Deploy. Semua permintaan penyedia AI diteruskan melalui edge Deno, menyembunyikan IP asli Anda.", + "Deploys an edge relay function to Vercel that proxies requests through Vercel's network.": "Mendeploy fungsi relay edge ke Vercel yang memproxy permintaan melalui jaringan Vercel.", + "Deploys an edge relay function to Vercel. All AI provider requests will be forwarded through Vercel's edge network, masking your real IP from providers.": "Mendeploy fungsi relay edge ke Vercel. Semua permintaan penyedia AI akan diteruskan melalui jaringan edge Vercel, menyembunyikan IP asli Anda dari penyedia.", + "Desktop": "Desktop", + "Detail": "Detail", + "Details": "Detail", + "Dimensions": "Dimensi", + "Disable": "Nonaktifkan", + "Disable All": "Nonaktifkan Semua", + "Disable Tailscale": "Nonaktifkan Tailscale", + "Disable Tunnel": "Nonaktifkan Tunnel", + "Disable connections with depleted quota on the current page": "Nonaktifkan koneksi dengan kuota habis di halaman saat ini", + "Disable provider": "Nonaktifkan penyedia", + "Disable this model": "Nonaktifkan model ini", + "Disabled": "Dinonaktifkan", + "Disabling...": "Menonaktifkan...", + "Disconnected from server": "Terputus dari server", + "Dismiss notification": "Tutup notifikasi", + "Display Name": "Nama Tampilan", + "Display language": "Bahasa tampilan", + "Docs": "Dokumentasi", + "Documentation": "Dokumentasi", + "Domain:": "Domain:", + "Donate": "Donasi", + "Done": "Selesai", + "Download": "Unduh", + "Download Backup": "Unduh Cadangan", + "Drag to reorder": "Seret untuk mengubah urutan", + "Easy Setup": "Pengaturan Mudah", + "Edit": "Sunting", + "Edit Combo": "Sunting Kombinasi", + "Edit Connection": "Sunting Koneksi", + "Edit Pricing": "Sunting Harga", + "Edit Proxy Pool": "Sunting Pool Proxy", + "Edit connection": "Sunting koneksi", + "Edit hosts file manually to add the following entries:": "Edit file hosts secara manual untuk menambahkan entri berikut:", + "Email": "Email", + "Embedding": "Embedding", + "Embeddings": "Embedding", + "Enable": "Aktifkan", + "Enable DNS per tool below to activate interception": "Aktifkan DNS untuk setiap alat di bawah untuk mengaktifkan intersepsi", + "Enable DNS to edit model mappings": "Aktifkan DNS untuk mengedit pemetaan model", "Enable Observability": "Aktifkan Observabilitas", - "Turn request detail recording on/off globally": "Aktifkan/nonaktifkan pencatatan detail permintaan secara global", + "Enable OpenAI API": "Aktifkan API OpenAI", + "Enable Tunnel": "Aktifkan Tunnel", + "Enable connections that still have quota on the current page": "Aktifkan koneksi yang masih memiliki kuota di halaman saat ini", + "Enable provider": "Aktifkan penyedia", + "Enable proxy for OAuth + provider outbound requests.": "Aktifkan proxy untuk permintaan keluar OAuth + penyedia.", + "Encrypted": "Terenkripsi", + "End Date": "Tanggal Akhir", + "End-to-end TLS via Cloudflare": "TLS end-to-end melalui Cloudflare", + "Endpoint": "Titik Akhir", + "Endpoint & Key": "Titik Akhir & Kunci", + "Endpoint is exposed without an API key.": "Titik akhir terekspos tanpa kunci API.", + "Enter current password": "Masukkan kata sandi saat ini", + "Enter model id": "Masukkan ID model", + "Enter model id (provider-specific)": "Masukkan ID model (khusus penyedia)", + "Enter new API key": "Masukkan kunci API baru", + "Enter new password": "Masukkan kata sandi baru", + "Enter or pick API key": "Masukkan atau pilih kunci API", + "Enter password": "Masukkan kata sandi", + "Enter sudo password": "Masukkan kata sandi sudo", + "Enter the model ID exactly as your compatible endpoint expects it. This model will be saved as the connection default.": "Masukkan ID model persis seperti yang diharapkan titik akhir kompatibel Anda. Model ini akan disimpan sebagai bawaan koneksi.", + "Enter your API key": "Masukkan kunci API Anda", + "Enter your current password to": "Masukkan kata sandi saat ini untuk", + "Enter your password to access the dashboard": "Masukkan kata sandi Anda untuk mengakses dasbor", + "Error": "Kesalahan", + "Est. Cost": "Perk. Biaya", + "Estimated, not actual billing": "Perkiraan, bukan penagihan aktual", + "Everything you need to manage your AI infrastructure efficiently.": "Semua yang Anda butuhkan untuk mengelola infrastruktur AI Anda secara efisien.", + "Everything you need to manage your AI infrastructure in one place, built for scale.": "Semua yang Anda butuhkan untuk mengelola infrastruktur AI di satu tempat, dibangun untuk skalabilitas.", + "Example": "Contoh", + "Experimental": "Eksperimental", + "Expires At": "Kedaluwarsa Pada", + "Expiring first": "Kedaluwarsa terlebih dahulu", + "Expiring-first currently reorders accounts inside the current page. Cross-page ordering still follows backend pagination.": "\"Kedaluwarsa terlebih dahulu\" saat ini mengurutkan ulang akun di dalam halaman saat ini. Pengurutan lintas halaman masih mengikuti paginasi backend.", + "Expose your local 9Router to the internet. No port forwarding, no static IP needed. Share endpoint URL with your team or use it in Cursor, Cline, and other AI tools from anywhere.": "Terekspos 9Router lokal Anda ke internet. Tidak perlu port forwarding, tidak perlu IP statis. Bagikan URL titik akhir dengan tim Anda atau gunakan di Cursor, Cline, dan alat AI lainnya dari mana saja.", + "Factory Droid - Manual Configuration": "Factory Droid - Konfigurasi Manual", + "Factory Droid AI Assistant": "Asisten AI Factory Droid", + "Factory Droid CLI not detected locally": "Factory Droid CLI tidak terdeteksi secara lokal", + "Factory Droid CLI not installed": "Factory Droid CLI tidak terinstal", + "Fail request if proxy is unreachable instead of falling back to direct.": "Gagalkan permintaan jika proxy tidak dapat dijangkau alih-alih kembali ke koneksi langsung.", + "Failed to apply settings": "Gagal menerapkan pengaturan", + "Failed to create combo": "Gagal membuat kombinasi", + "Failed to load changelog:": "Gagal memuat catatan perubahan:", + "Failed to load usage statistics.": "Gagal memuat statistik penggunaan.", + "Failed to reset settings": "Gagal mengatur ulang pengaturan", + "Failed to set alias": "Gagal mengatur alias", + "Failed to update combo": "Gagal memperbarui kombinasi", + "Failed to update password": "Gagal memperbarui kata sandi", + "Failed to update proxy settings": "Gagal memperbarui pengaturan proxy", + "Fallback": "Fallback", + "Fallback — tries models in order (next on failure)": "Fallback — mencoba model secara berurutan (berikutnya saat gagal)", + "Fallback — try in order": "Fallback — coba secara berurutan", + "Features": "Fitur", + "Fetch Qoder Models": "Ambil Model Qoder", + "Fetching...": "Mengambil...", + "Files": "File", + "Filter accounts by status": "Filter akun berdasarkan status", + "Filter naming": "Filter penamaan", + "Filter naming requests": "Filter permintaan penamaan", + "Filter quota providers": "Filter penyedia kuota", + "Find MCPs →": "Temukan MCP →", + "Find your Account ID in the right sidebar of": "Temukan ID Akun Anda di bilah sisi kanan", + "Find your Account ID in the right sidebar of dash.cloudflare.com": "Temukan ID Akun Anda di bilah sisi kanan dash.cloudflare.com", + "First Page": "Halaman Pertama", + "Flush Interval (ms)": "Interval Flush (ms)", + "For enterprise users with custom AWS IAM Identity Center.": "Untuk pengguna perusahaan dengan Pusat Identitas AWS IAM kustom.", + "Forgot password? Open": "Lupa kata sandi? Buka", + "Format": "Format", + "Found on the right side of the Cloudflare dashboard overview page.": "Ditemukan di sisi kanan halaman ringkasan dasbor Cloudflare.", + "Free": "Gratis", + "Free & Free Tier Providers": "Penyedia Gratis & Gratis Tier", + "Free Providers": "Penyedia Gratis", + "Free Tier": "Tier Gratis", + "Free Tier Providers": "Penyedia Tier Gratis", + "Free tier: 100,000 requests per day": "Tier gratis: 100.000 permintaan per hari", + "Free tier: 100GB bandwidth/month, 500K edge invocations": "Tier gratis: 100GB bandwidth/bulan, 500K pemanggilan edge", + "Free tier: 1M requests & 100GiB outbound traffic per month": "Tier gratis: 1M permintaan & 100GiB lalu lintas keluar per bulan", + "Fresh API key obtained": "Kunci API baru diperoleh", + "Full shell access": "Akses shell penuh", + "Fusion": "Fusi", + "Fusion — panel + judge": "Fusi — panel + juri", + "Fusion — queries all models in parallel, then a judge synthesizes one answer": "Fusi — menanyakan semua model secara paralel, lalu juri mensintesis satu jawaban", + "Get 9Remote": "Dapatkan 9Remote", + "Get API Key": "Dapatkan Kunci API", + "Get API Key →": "Dapatkan Kunci API →", + "Get Started": "Mulai", + "Get Started in 30 Seconds": "Mulai dalam 30 Detik", + "Get started": "Mulai", + "Get started in seconds. Just install, open, and route.": "Mulai dalam hitungan detik. Cukup instal, buka, dan rutekan.", + "Get token →": "Dapatkan token →", + "GitHub": "GitHub", + "GitHub Account": "Akun GitHub", + "GitHub Copilot - Manual Configuration": "GitHub Copilot - Konfigurasi Manual", + "GitHub Copilot IDE with MITM": "IDE GitHub Copilot dengan MITM", + "GitLab Access Tokens": "Token Akses GitLab", + "GitLab Applications": "Aplikasi GitLab", + "GitLab Base URL": "URL Dasar GitLab", + "Go to": "Buka", + "Go to Roo Settings panel": "Buka panel Pengaturan Roo", + "Google Account": "Akun Google", + "Google Antigravity IDE with MITM": "IDE Google Antigravity dengan MITM", + "Granted At": "Diberikan Pada", + "Group models under one name, then pick a strategy per combo:": "Kelompokkan model di bawah satu nama, lalu pilih strategi per kombinasi:", + "Headroom proxy is reachable. You can enable the token saver.": "Proxy Headroom dapat dijangkau. Anda dapat mengaktifkan penghemat token.", + "Help Center": "Pusat Bantuan", + "Hermes Agent - Manual Configuration": "Hermes Agent - Konfigurasi Manual", + "Hermes Agent not detected locally": "Hermes Agent tidak terdeteksi secara lokal", + "Hide": "Sembunyikan", + "Hide key": "Sembunyikan kunci", + "High performance global routing and IP masking via Cloudflare Workers": "Rute global berkinerja tinggi dan penyembunyian IP melalui Cloudflare Workers", + "High-performance Rust-based coding agent harness": "Rangka agen pemrograman berbasis Rust berkinerja tinggi", + "History": "Riwayat", + "How 9Router Works": "Cara Kerja 9Router", + "How Pricing Works": "Cara Kerja Penetapan Harga", + "How it Works": "Cara Kerja", + "How it works:": "Cara kerjanya:", + "How to Install": "Cara Instalasi", + "How to generate API token:": "Cara membuat token API:", + "How to generate your API Token:": "Cara membuat Token API Anda:", + "How to get cookie:": "Cara mendapatkan cookie:", + "ID:": "ID:", + "IDC Start URL": "URL Mulai IDC", + "If provider lacks /models endpoint, enter a model ID to validate via chat/completions instead.": "Jika penyedia kekurangan titik akhir /models, masukkan ID model untuk memvalidasi melalui chat/completions sebagai gantinya.", + "Image Generation": "Pembuatan Gambar", + "Image to Text": "Gambar ke Teks", + "Import": "Impor", + "Import Backup": "Impor Cadangan", + "Import CLIProxyAPI JSON": "Impor JSON CLIProxyAPI", + "Import Token": "Impor Token", + "Importing...": "Mengimpor...", + "In": "Masukan", + "In / Out": "Masukan / Keluaran", + "Inactive": "Nonaktif", + "Inactive pools are ignored by runtime resolution.": "Pool nonaktif diabaikan oleh resolusi runtime.", + "Inc. All rights reserved.": "Inc. Hak cipta dilindungi.", + "Initializing...": "Menginisialisasi...", + "Input": "Masukan", + "Input Cost": "Biaya Masukan", + "Input Tokens": "Token Masukan", + "Input Tokens:": "Token Masukan:", + "Input:": "Masukan:", + "Install 9Router": "Instal 9Router", + "Install 9Router, configure your providers via web dashboard, and start routing AI requests.": "Instal 9Router, konfigurasi penyedia Anda melalui dasbor web, dan mulai merutekan permintaan AI.", + "Install Chrome extension": "Instal ekstensi Chrome", + "Install Cline VS Code extension or CLI from": "Instal ekstensi Cline VS Code atau CLI dari", + "Install Kilo Code from": "Instal Kilo Code dari", + "Install Qwen Code": "Instal Qwen Code", + "Install Tailscale": "Instal Tailscale", + "Install command:": "Perintah instalasi:", + "Install jcode to enable automatic configuration:": "Instal jcode untuk mengaktifkan konfigurasi otomatis:", + "Install the Amp CLI using the package manager supported by your environment.": "Instal Amp CLI menggunakan manajer paket yang didukung oleh lingkungan Anda.", + "Install then click Start:": "Instal lalu klik Mulai:", + "Install via npm:": "Instal melalui npm:", + "Installation Guide": "Panduan Instalasi", + "Installing Tailscale...": "Menginstal Tailscale...", + "Interactive diagram visible on desktop": "Diagram interaktif terlihat di desktop", + "Intercept CLI tool traffic and route through 9Router": "Intersep lalu lintas alat CLI dan rutekan melalui 9Router", + "Intercepts Antigravity traffic via DNS redirect, letting you reroute models through 9Router.": "Mencegat lalu lintas Antigravity melalui pengalihan DNS, memungkinkan Anda merutekan ulang model melalui 9Router.", + "Intercepts Claude Code's topic-naming requests and returns a fake response locally, saving API tokens.": "Mencegat permintaan penamaan topik Claude Code dan mengembalikan respons palsu secara lokal, menghemat token API.", + "Invalid": "Tidak Valid", + "Invalid password": "Kata sandi tidak valid", + "Issuer URL": "URL Penerbit", + "JSON Response": "Respons JSON", + "Join developers who are streamlining their AI integrations with 9Router. Open source and free to start.": "Bergabunglah dengan pengembang yang menyederhanakan integrasi AI mereka dengan 9Router. Sumber terbuka dan gratis untuk memulai.", + "Judge": "Juri", + "Just now": "Baru saja", + "KB per field": "KB per bidang", + "Keep the legacy password login.": "Pertahankan login kata sandi lama.", + "Key Name": "Nama Kunci", + "KiRo dashboard": "Dasbor KiRo", + "Kill & Start": "Hentikan & Mulai", + "Kill this process to start MITM Server?": "Hentikan proses ini untuk memulai Server MITM?", + "Kilo Code - Manual Configuration": "Kilo Code - Konfigurasi Manual", + "Kilo Code AI Assistant": "Asisten AI Kilo Code", + "Kilo Code not detected locally": "Kilo Code tidak terdeteksi secara lokal", + "Kimi": "Kimi", + "Kiro AI": "Kiro AI", + "Kiro IDE not detected. Please paste your refresh token manually.": "Kiro IDE tidak terdeteksi. Silakan tempel token penyegaran Anda secara manual.", + "Kiro IDE with MITM": "Kiro IDE dengan MITM", + "Language": "Bahasa", + "Languages": "Bahasa", + "Last Page": "Halaman Terakhir", + "Last Used": "Terakhir Digunakan", + "Last tested:": "Terakhir diuji:", + "Last updated:": "Terakhir diperbarui:", + "Latency": "Latensi", + "Latency:": "Latensi:", + "Lazy senior dev": "Developer senior malas", + "Lean": "Ringkas", + "Leave blank to keep existing secret": "Kosongkan untuk mempertahankan rahasia yang ada", + "Leave blank to use": "Kosongkan untuk menggunakan", + "Leave empty for public PKCE app": "Kosongkan untuk aplikasi PKCE publik", + "Leave empty to inherit existing env proxy (if any).": "Kosongkan untuk mewarisi proxy env yang ada (jika ada).", + "Legacy manual proxy fields are still accepted by API for backward compatibility.": "Bidang proxy manual lama masih diterima oleh API untuk kompatibilitas mundur.", + "Legacy:": "Lama:", + "Legal": "Hukum", + "Live server console output": "Output konsol server langsung", + "Load": "Muat", + "Loading logs...": "Memuat log...", + "Loading models from provider...": "Memuat model dari penyedia...", + "Loading pricing data...": "Memuat data harga...", + "Loading registry...": "Memuat registri...", + "Loading reset credits...": "Memuat kredit reset...", + "Loading...": "Memuat...", + "Local": "Lokal", + "Local Mode": "Mode Lokal", + "Local Mode - All data stored on your machine": "Mode Lokal - Semua data disimpan di mesin Anda", + "Local Plugins": "Plugin Lokal", + "Locked. Retry in": "Terkunci. Coba lagi dalam", + "Login": "Masuk", + "Login Button Label": "Label Tombol Login", + "Login URL": "URL Login", + "Login to your account": "Masuk ke akun Anda", + "Login with your GitHub account (manual callback).": "Masuk dengan akun GitHub Anda (callback manual).", + "Login with your Google account (manual callback).": "Masuk dengan akun Google Anda (callback manual).", + "Logout": "Keluar", + "Logs": "Log", + "Logs are loaded from the request history database.": "Log dimuat dari database riwayat permintaan.", + "Logs are saved to log.txt in the application data directory.": "Log disimpan ke log.txt di direktori data aplikasi.", + "MIT License": "Lisensi MIT", + "MITM": "MITM", + "MITM Proxy": "Proxy MITM", + "MITM Server": "Server MITM", + "MITM Tools": "Alat MITM", + "Machine ID": "ID Mesin", + "Machine ID will be auto-filled...": "ID Mesin akan diisi otomatis...", + "Make sure Cursor IDE has been opened at least once, then click": "Pastikan Cursor IDE telah dibuka setidaknya sekali, lalu klik", + "Manage": "Kelola", + "Manage reusable per-connection proxies and bind them to provider connections.": "Kelola proxy per- koneksi yang dapat digunakan ulang dan ikat ke koneksi penyedia.", + "Manage your AI provider connections": "Kelola koneksi penyedia AI Anda", + "Manage your Embedding providers": "Kelola penyedia Embedding Anda", + "Manage your Image to Text providers": "Kelola penyedia Gambar ke Teks Anda", + "Manage your Music providers": "Kelola penyedia Musik Anda", + "Manage your Speech To Text providers": "Kelola penyedia Suara ke Teks Anda", + "Manage your Text To Speech providers": "Kelola penyedia Teks ke Suara Anda", + "Manage your Text to Image providers": "Kelola penyedia Teks ke Gambar Anda", + "Manage your Video providers": "Kelola penyedia Video Anda", + "Manage your Web Fetch providers": "Kelola penyedia Pengambilan Web Anda", + "Manage your Web Search providers": "Kelola penyedia Pencarian Web Anda", + "Manage your preferences": "Kelola preferensi Anda", + "Manage your proxy pool configurations": "Kelola konfigurasi pool proxy Anda", + "Manual / current endpoint": "Manual / titik akhir saat ini", + "Manual Callback Required": "Callback Manual Diperlukan", + "Manual Config": "Konfigurasi Manual", + "Manual configuration is still available if 9router is deployed on a remote server.": "Konfigurasi manual masih tersedia jika 9router di-deploy di server remote.", + "Map Amp shorthand names such as g25p or cs45 to 9Router aliases in your local config.": "Pemetaan nama pendek Amp seperti g25p atau cs45 ke alias 9Router di konfigurasi lokal Anda.", + "Mask (URL)": "Mask (URL)", + "Max JSON Size (KB)": "Ukuran JSON Maks (KB)", "Max Records": "Rekam Maksimal", "Maximum request detail records to keep (older records are auto-deleted)": "Rekam detail permintaan maksimal untuk disimpan (rekam lama otomatis dihapus)", - "Batch Size": "Ukuran Batch", - "Number of items to accumulate before writing to database (higher = better performance)": "Jumlah item yang diakumulasikan sebelum menulis ke database (lebih tinggi = performa lebih baik)", - "Flush Interval (ms)": "Interval Flush (ms)", - "Maximum time to wait before flushing buffer (prevents data loss during low traffic)": "Waktu maksimum untuk menunggu sebelum mem-flush buffer (mencegah kehilangan data saat lalu lintas rendah)", - "Max JSON Size (KB)": "Ukuran JSON Maks (KB)", "Maximum size for each JSON field (request/response) before truncation": "Ukuran maksimum untuk setiap bidang JSON (permintaan/respons) sebelum pemotongan", - "All data stored on your machine": "Semua data disimpan di mesin Anda", - "MITM Server": "Server MITM", - "Running": "Berjalan", - "Stopped": "Dihentikan", - "Cert": "Sertifikat", - "Server": "Server", + "Maximum time to wait before flushing buffer (prevents data loss during low traffic)": "Waktu maksimum untuk menunggu sebelum mem-flush buffer (mencegah kehilangan data saat lalu lintas rendah)", + "Media Providers": "Penyedia Media", + "Menu": "Menu", + "Message AI": "Pesan AI", + "Messages": "Pesan", + "Messages API": "API Pesan", + "MiniMax": "MiniMax", + "Model": "Model", + "Model Fallback": "Fallback Model", + "Model ID": "ID Model", + "Model ID (from OpenRouter)": "ID Model (dari OpenRouter)", + "Model ID (optional)": "ID Model (opsional)", + "Model Status": "Status Model", + "Model combos": "Kombinasi model", + "Model combos with fallback": "Kombinasi model dengan fallback", + "Model is reachable": "Model dapat dijangkau", + "Model list is filtered from connected providers.": "Daftar model difilter dari penyedia yang terhubung.", + "Model mappings will be available soon.": "Pemetaan model akan segera tersedia.", + "Model not reachable": "Model tidak dapat dijangkau", + "Model:": "Model:", + "Models": "Model", + "Monitor your API usage, token consumption, and request logs": "Pantau penggunaan API, konsumsi token, dan log permintaan Anda", + "More on GitHub": "Lebih banyak di GitHub", + "Move down": "Pindah ke bawah", + "Move up": "Pindah ke atas", + "Music": "Musik", + "My Profile": "Profil Saya", + "N/A": "Tidak Tersedia", + "NPM": "NPM", + "Name": "Nama", + "Name is required": "Nama wajib diisi", + "Native CLI tool support for Cursor, Claude, Copilot, and more.": "Dukungan alat CLI native untuk Cursor, Claude, Copilot, dan lainnya.", + "Navigate to home": "Navigasi ke beranda", + "Network": "Jaringan", + "Network Error": "Kesalahan Jaringan", + "Network error": "Kesalahan jaringan", + "Never": "Tidak Pernah", + "New Password": "Kata Sandi Baru", + "New password": "Kata sandi baru", + "Next": "Berikutnya", + "Next accounts page": "Halaman akun berikutnya", + "No API keys - Create one in Keys page": "Tidak ada kunci API - Buat satu di halaman Kunci", + "No API keys yet": "Belum ada kunci API", + "No MCPs added": "Tidak ada MCP yang ditambahkan", + "No Providers Connected": "Tidak Ada Penyedia Terhubung", + "No Proxy": "Tidak Ada Proxy", + "No active connections found for this group.": "Tidak ditemukan koneksi aktif untuk grup ini.", + "No active providers": "Tidak ada penyedia aktif", + "No active proxy pools available. Create one in Proxy Pools page first.": "Tidak ada pool proxy aktif yang tersedia. Buat satu di halaman Pool Proxy terlebih dahulu.", + "No authentication required": "Tidak diperlukan autentikasi", + "No combos yet": "Belum ada kombinasi", + "No combos yet.": "Belum ada kombinasi.", + "No compatible providers added yet": "Belum ada penyedia kompatibel yang ditambahkan", + "No connections": "Tidak ada koneksi", + "No connections yet": "Belum ada koneksi", + "No console logs yet.": "Belum ada log konsol.", + "No conversations yet.": "Belum ada percakapan.", + "No custom providers": "Tidak ada penyedia kustom", + "No custom providers — use buttons above to add OpenAI/Anthropic compatible endpoints": "Tidak ada penyedia kustom — gunakan tombol di atas untuk menambahkan titik akhir kompatibel OpenAI/Anthropic", + "No data for this period": "Tidak ada data untuk periode ini", + "No key configured": "Tidak ada kunci yang dikonfigurasi", + "No language selected": "Tidak ada bahasa yang dipilih", + "No languages found.": "Tidak ada bahasa yang ditemukan.", + "No logs recorded yet.": "Belum ada log yang direkam.", + "No model selected.": "Tidak ada model yang dipilih.", + "No models": "Belum ada model", + "No models added yet": "Belum ada model yang ditambahkan", + "No models configured": "Tidak ada model yang dikonfigurasi", + "No models found": "Tidak ada model yang ditemukan", + "No models match your filter.": "Tidak ada model yang cocok dengan filter Anda.", + "No models selected": "Tidak ada model yang dipilih", + "No port forwarding needed": "Tidak perlu port forwarding", + "No pricing data available": "Tidak ada data harga yang tersedia", + "No providers connected": "Tidak ada penyedia yang terhubung", + "No providers match your search": "Tidak ada penyedia yang cocok dengan pencarian Anda", + "No providers support": "Tidak ada penyedia yang mendukung", + "No providers yet.": "Belum ada penyedia.", + "No providers.": "Belum ada penyedia.", + "No proxy pool entries yet": "Belum ada entri pool proxy", + "No proxy:": "Tidak ada proxy:", + "No quota data available": "Tidak ada data kuota yang tersedia", + "No request details found": "Tidak ada detail permintaan yang ditemukan", + "No requests yet.": "Belum ada permintaan.", + "No reset credit details returned for this account.": "Tidak ada detail kredit reset yang dikembalikan untuk akun ini.", + "No results": "Tidak ada hasil", + "No servers match filter": "Tidak ada server yang cocok dengan filter", + "No tools advertised by server.": "Tidak ada alat yang diiklankan oleh server.", + "No usage yet.": "Belum ada penggunaan.", + "None": "Tidak Ada", + "None (unbind all)": "Tidak Ada (lepas semua)", + "Not configured": "Tidak dikonfigurasi", + "Not installed": "Tidak terinstal", + "Notice": "Pemberitahuan", + "Nous Research self-improving AI agent": "Agen AI peningkatan diri Nous Research", + "Number of items to accumulate before writing to database (higher = better performance)": "Jumlah item yang diakumulasikan sebelum menulis ke database (lebih tinggi = performa lebih baik)", + "OAuth": "OAuth", + "OAuth & API Keys": "OAuth & Kunci API", + "OAuth Account": "Akun OAuth", + "OAuth App": "Aplikasi OAuth", + "OAuth Providers": "Penyedia OAuth", + "OAuth required": "OAuth diperlukan", + "OIDC Dashboard Login": "Login Dasbor OIDC", + "OIDC active": "OIDC aktif", + "OIDC login is currently active. Password login is disabled until you switch back.": "Login OIDC saat ini aktif. Login kata sandi dinonaktifkan sampai Anda beralih kembali.", + "OIDC login is enabled, but the issuer/client fields are not configured yet. Password login is still available for recovery.": "Login OIDC diaktifkan, tetapi bidang penerbit/klien belum dikonfigurasi. Login kata sandi masih tersedia untuk pemulihan.", + "OIDC only": "Hanya OIDC", + "Observability": "Observabilitas", + "Office Proxy": "Proxy Kantor", + "Ollama Host URL": "URL Host Ollama", + "One Endpoint for": "Satu Titik Akhir untuk", + "One key per line. Format:": "Satu kunci per baris. Format:", + "One-to-one (rotate)": "Satu-ke-satu (rotasi)", + "Only from connected providers": "Hanya dari penyedia yang terhubung", + "Only letters, numbers, - and _ allowed": "Hanya huruf, angka, - dan _ yang diperbolehkan", + "Only letters, numbers, -, _ and .": "Hanya huruf, angka, -, _ dan .", + "Only letters, numbers, -, _ and . allowed": "Hanya huruf, angka, -, _ dan . yang diperbolehkan", + "Only one connection is allowed per compatible node. Add another node if you need more connections.": "Hanya satu koneksi yang diperbolehkan per node kompatibel. Tambah node lain jika Anda membutuhkan lebih banyak koneksi.", + "Open": "Buka", + "Open Claude Desktop → Help → Troubleshooting → Enable Developer mode → Configure third-party inference, then return here.": "Buka Claude Desktop → Bantuan → Pemecahan Masalah → Aktifkan mode Pengembang → Konfigurasi inferensi pihak ketiga, lalu kembali ke sini.", + "Open Claw - Manual Configuration": "Open Claw - Konfigurasi Manual", + "Open Claw AI Assistant": "Asisten AI Open Claw", + "Open Claw CLI not detected locally": "Open Claw CLI tidak terdeteksi secara lokal", + "Open Claw CLI not installed": "Open Claw CLI tidak terinstal", + "Open Continue configuration file": "Buka file konfigurasi Continue", + "Open Dashboard": "Buka Dasbor", + "Open DevTools (F12) → Application/Storage → Cookies": "Buka DevTools (F12) → Aplikasi/Penyimpanan → Cookie", + "Open Settings": "Buka Pengaturan", + "Open platform.iflow.cn in your browser": "Buka platform.iflow.cn di browser Anda", + "OpenAI / ElevenLabs / Edge / Google / Deepgram voices.": "Suara OpenAI / ElevenLabs / Edge / Google / Deepgram.", + "OpenAI Codex CLI": "OpenAI Codex CLI", + "OpenAI Compatible (Prod)": "Kompatibel OpenAI (Produksi)", + "OpenAI Compatible Details": "Detail Kompatibel OpenAI", + "OpenAI Intermediate": "Perantara OpenAI", + "OpenAI Response": "Respons OpenAI", + "OpenCode - Manual Configuration": "OpenCode - Konfigurasi Manual", + "OpenCode AI Terminal Assistant": "Asisten Terminal AI OpenCode", + "OpenCode CLI not detected locally": "OpenCode CLI tidak terdeteksi secara lokal", + "OpenCode CLI not installed": "OpenCode CLI tidak terinstal", + "OpenRouter": "OpenRouter", + "OpenRouter supports any model. Add models and create aliases for quick access.": "OpenRouter mendukung model apa pun. Tambah model dan buat alias untuk akses cepat.", + "Optional SSO via Authentik/Keycloak/Google": "Opsional SSO melalui Authentik/Keycloak/Google", + "Or paste callback URL manually": "Atau tempel URL callback secara manual", + "Organization": "Organisasi", + "Organization Domain": "Domain Organisasi", + "Organization ID": "ID Organisasi", + "Organization Token": "Token Organisasi", + "Organization Tokens": "Token Organisasi", + "Other": "Lainnya", + "Our engine analyzes the prompt and routes through your subscription, cheap, and free provider tiers with automatic fallback.": "Mesin kami menganalisis prompt dan merutekan melalui langganan, penyedia murah, dan tier gratis dengan fallback otomatis.", + "Our engine analyzes the prompt, checks provider health, and routes for lowest latency or cost.": "Mesin kami menganalisis prompt, memeriksa kesehatan penyedia, dan merutekan untuk latensi atau biaya terendah.", + "Out": "Keluaran", + "Outbound Proxy": "Proxy Keluar", + "Output": "Keluaran", + "Output Cost": "Biaya Keluaran", + "Output Format": "Format Keluaran", + "Output Tokens": "Token Keluaran", + "Output Tokens:": "Token Keluaran:", + "Output:": "Keluaran:", + "Overview": "Ringkasan", + "Paid": "Berbayar", + "Partial preview": "Pratinjau sebagian", + "Password": "Kata Sandi", + "Password + OIDC active": "Kata Sandi + OIDC aktif", + "Password and OIDC login are both active.": "Login kata sandi dan OIDC keduanya aktif.", + "Password and OIDC login are both enabled.": "Login kata sandi dan OIDC keduanya diaktifkan.", + "Password only": "Hanya kata sandi", + "Password updated successfully": "Kata sandi berhasil diperbarui", + "Passwords do not match": "Kata sandi tidak cocok", + "Paste Proxy List (One per line)": "Tempel Daftar Proxy (Satu per baris)", + "Paste a long-lived Kiro/CodeWhisperer API key. It is validated against AWS and stored directly as a bearer credential (no refresh).": "Tempel kunci API Kiro/CodeWhisperer jangka panjang. Ini divalidasi terhadap AWS dan disimpan langsung sebagai kredensial bearer (tanpa penyegaran).", + "Paste external_idp auth JSON from CLIProxyAPI/Kiro Microsoft login.": "Tempel JSON autentikasi external_idp dari login CLIProxyAPI/Kiro Microsoft.", + "Paste it below": "Tempel di bawah ini", + "Paste refresh token from Kiro IDE.": "Tempel token penyegaran dari Kiro IDE.", + "Paste the Kiro CLIProxyAPI auth JSON containing auth_method=external_idp. Only Microsoft login token endpoints are accepted.": "Tempel JSON autentikasi Kiro CLIProxyAPI yang berisi auth_method=external_idp. Hanya titik akhir token login Microsoft yang diterima.", + "Paste the URL from your browser address bar": "Tempel URL dari bilah alamat browser Anda", + "Paste the command into your terminal and press Enter.": "Tempel perintah ke terminal Anda dan tekan Enter.", + "Paste this to your AI:": "Tempel ini ke AI Anda:", + "Paste your Kiro API key...": "Tempel kunci API Kiro Anda...", + "Pause API Key": "Jeda Kunci API", + "Pause key": "Jeda kunci", + "Paused": "Dijeda", + "Permissions": "Izin", + "Personal Access Token": "Token Akses Pribadi", + "Pick the model that fuses panel answers": "Pilih model yang menggabungkan jawaban panel", + "Please add an active Qoder connection first": "Silakan tambah koneksi Qoder yang aktif terlebih dahulu", + "Please add and connect providers first to configure CLI tools.": "Silakan tambah dan hubungkan penyedia terlebih dahulu untuk mengkonfigurasi alat CLI.", + "Please copy the URL from the address bar and paste it in the application.": "Silakan salin URL dari bilah alamat dan tempel di aplikasi.", + "Please enter a Proxy URL to test": "Masukkan URL Proxy untuk diuji", + "Please install Claude CLI to use this feature.": "Silakan instal Claude CLI untuk menggunakan fitur ini.", + "Please install Codex CLI to use auto-apply feature.": "Silakan instal Codex CLI untuk menggunakan fitur penerapan otomatis.", + "Please install Factory Droid CLI to use this feature.": "Silakan instal Factory Droid CLI untuk menggunakan fitur ini.", + "Please install Open Claw CLI to use this feature.": "Silakan instal Open Claw CLI untuk menggunakan fitur ini.", + "Please install OpenCode CLI to use auto-apply feature.": "Silakan instal OpenCode CLI untuk menggunakan fitur penerapan otomatis.", + "Please wait while we complete the authorization.": "Silakan tunggu sementara kami menyelesaikan otorisasi.", + "Point your CLI tools to http://localhost:20128": "Arahkan alat CLI Anda ke http://localhost:20128", + "Pool:": "Pool:", + "Popup blocked? Enter URL manually": "Popup diblokir? Masukkan URL secara manual", + "Port 443 Already In Use": "Port 443 Sudah Digunakan", + "Port 443 is currently used by another process:": "Port 443 saat ini digunakan oleh proses lain:", + "Powerful Features": "Fitur Canggih", + "Prefix": "Awalan", + "Preset": "Preset", + "Prev": "Sebelumnya", + "Preview": "Pratinjau", + "Previous accounts page": "Halaman akun sebelumnya", + "Pricing": "Harga", + "Pricing Configuration": "Konfigurasi Harga", + "Pricing Format:": "Format Harga:", + "Pricing Rates Format": "Format Tarif Harga", + "Pricing Settings": "Pengaturan Harga", + "Priority": "Prioritas", + "Privacy Policy": "Kebijakan Privasi", + "Probing server for tools...": "Mengeksplorasi server untuk alat...", + "Processing...": "Memproses...", + "Product": "Produk", + "Production Key": "Kunci Produksi", + "Project Name": "Nama Proyek", + "Prompt": "Prompt", + "Provider": "Penyedia", + "Provider Details": "Detail Penyedia", + "Provider Limits": "Batas Penyedia", + "Provider Response": "Respons Penyedia", + "Provider not found": "Penyedia tidak ditemukan", + "Provider test failed": "Pengujian penyedia gagal", + "Provider:": "Penyedia:", + "Providers": "Penyedia", + "Proxy": "Proxy", + "Proxy Action": "Tindakan Proxy", + "Proxy Pool": "Pool Proxy", + "Proxy Pools": "Pool Proxy", + "Proxy URL": "URL Proxy", + "Proxy disabled": "Proxy dinonaktifkan", + "Proxy enabled": "Proxy diaktifkan", + "Proxy pool created": "Pool proxy dibuat", + "Proxy pool deleted": "Pool proxy dihapus", + "Proxy pool updated": "Pool proxy diperbarui", + "Proxy settings applied": "Pengaturan proxy diterapkan", + "Proxy test OK": "Pengujian proxy berhasil", + "Proxy test failed": "Pengujian proxy gagal", + "Proxy test passed": "Pengujian proxy berhasil", "Purpose:": "Tujuan:", - "Use Antigravity IDE & GitHub Copilot → with ANY provider/model from 9Router": "Gunakan Antigravity IDE & GitHub Copilot → dengan PENYEDIA/model APA PUN dari 9Router", - "How it works:": "Cara kerjanya:", - "Antigravity/Copilot IDE request → DNS redirect to localhost:443 → MITM proxy intercepts → 9Router → response to Antigravity/Copilot": "Permintaan Antigravity/Copilot IDE → Pengalihan DNS ke localhost:443 → Proxy MITM mengintersep → 9Router → respons ke Antigravity/Copilot", - "No API keys — create one in Keys page": "Tidak ada kunci API — buat satu di halaman Keys", - "e.g. a warm, gentle voice, speaking slowly with a British accent": "mis.: suara hangat dan lembut, berbicara pelan dengan aksen Inggris", - "sk_9router (default)": "sk_9router (bawaan)", - "Server started": "Server dimulai", - "Failed to start server": "Gagal memulai server", - "Server stopped — all DNS cleared": "Server dihentikan — semua DNS dihapus", - "Failed to stop server": "Gagal menghentikan server", - "Sudo password is required": "Kata sandi sudo diperlukan", - "Stop Server": "Hentikan Server", + "Python >= 3.10 required for local managed mode. Install Python first, or use an external proxy URL.": "Python >= 3.10 diperlukan untuk mode lokal terkelola. Instal Python terlebih dahulu, atau gunakan URL proxy eksternal.", + "Python ≥ 3.10 required for local managed mode. Install Python first, or use an external proxy URL.": "Python ≥ 3.10 diperlukan untuk mode lokal terkelola. Instal Python terlebih dahulu, atau gunakan URL proxy eksternal.", + "Quota Tracker": "Pelacak Kuota", + "Qwen": "Qwen", + "Qwen Code supports multiple provider types (openai, anthropic, gemini) via modelProviders in settings.json. 9Router works as an OpenAI-compatible endpoint.": "Qwen Code mendukung beberapa jenis penyedia (openai, anthropic, gemini) melalui modelProviders di settings.json. 9Router bekerja sebagai titik akhir kompatibel OpenAI.", + "Qwen OAuth free tier was discontinued on 2026-04-15. Use 9Router with alicode/openrouter/anthropic/gemini providers instead.": "Tier gratis Qwen OAuth dihentikan pada 2026-04-15. Gunakan 9Router dengan penyedia alicode/openrouter/anthropic/gemini sebagai gantinya.", + "Rate Limited": "Dibatasi Tarif", + "Read Documentation": "Baca Dokumentasi", + "Reading from AWS SSO cache": "Membaca dari cache AWS SSO", + "Reading from Cursor IDE database": "Membaca dari database Cursor IDE", + "Ready": "Siap", + "Ready to Simplify Your AI Infrastructure?": "Siap Menyederhanakan Infrastruktur AI Anda?", + "Ready to route! ✓": "Siap merutekan! ✓", + "Ready! Requests route automatically through your configured providers.": "Siap! Permintaan dirutekan secara otomatis melalui penyedia yang Anda konfigurasi.", + "Reasoning": "Penalaran", + "Reasoning:": "Penalaran:", + "Recent Requests": "Permintaan Terkini", + "Recent chats": "Obrolan terkini", + "Recheck": "Periksa Ulang", + "Recommended for most users. Free AWS account required.": "Direkomendasikan untuk pengguna kebanyakan. Akun AWS gratis diperlukan.", + "Record request details for inspection in the logs view": "Rekam detail permintaan untuk pemeriksaan di tampilan log", + "Redirect URI": "URI Pengalihan", + "Ref Image (URL)": "Gambar Referensi (URL)", + "Refresh": "Segarkan", + "Refresh All": "Segarkan Semua", + "Refresh Token": "Token Penyegaran", + "Refresh all": "Segarkan semua", + "Refresh quota": "Segarkan kuota", + "Region": "Wilayah", + "Reload Page": "Muat Ulang Halaman", + "Reload VS Code after applying for changes to take effect.": "Muat ulang VS Code setelah menerapkan agar perubahan berlaku.", + "Remaining": "Tersisa", + "Remote": "Remote", + "Remove": "Hapus", + "Remove attachment": "Hapus lampiran", + "Remove custom model": "Hapus model kustom", + "Remove model": "Hapus model", + "Replaces built-in WebSearch/WebFetch. Auto-strips duplicates from tool list.": "Menggantikan WebSearch/WebFetch bawaan. Otomatis menghapus duplikat dari daftar alat.", + "Replay request flow — matches log files": "Putar ulang alur permintaan — cocok dengan file log", + "Request": "Permintaan", + "Request Details": "Detail Permintaan", + "Request Logs": "Log Permintaan", + "Requests": "Permintaan", + "Requests without a valid key will be rejected": "Permintaan tanpa kunci yang valid akan ditolak", + "Require API key": "Memerlukan kunci API", + "Require OIDC for dashboard access.": "Memerlukan OIDC untuk akses dasbor.", + "Require login": "Memerlukan Login", + "Required for SSL certificate and DNS configuration": "Diperlukan untuk sertifikat SSL dan konfigurasi DNS", + "Required for SSL certificate and server startup": "Diperlukan untuk sertifikat SSL dan startup server", + "Required to modify /etc/hosts and flush DNS cache": "Diperlukan untuk mengubah /etc/hosts dan flush cache DNS", + "Required. A friendly label for this node.": "Wajib. Label ramah untuk node ini.", + "Required. Used as the provider prefix for model IDs.": "Wajib. Digunakan sebagai awalan penyedia untuk ID model.", + "Requires \"Workers Scripts: Edit\" permission.": "Memerlukan izin \"Workers Scripts: Edit\".", + "Requires Cloudflare Account ID and a Workers API Token (Edit Workers permission)": "Memerlukan ID Akun Cloudflare dan Token API Workers (izin Edit Workers)", + "Requires Cursor Pro account to use this feature.": "Memerlukan akun Cursor Pro untuk menggunakan fitur ini.", + "Requires jcode installed. Install via: curl -fsSL https://raw.githubusercontent.com/1jehuang/jcode/master/scripts/install.sh | bash": "Memerlukan jcode terinstal. Instal melalui: curl -fsSL https://raw.githubusercontent.com/1jehuang/jcode/master/scripts/install.sh | bash", + "Requires outbound port 7844 (TCP/UDP). Connection may take 10-30s.": "Memerlukan port keluar 7844 (TCP/UDP). Koneksi mungkin memakan waktu 10-30 detik.", + "Reset": "Atur Ulang", + "Reset Codex limit?": "Atur ulang batas Codex?", + "Reset Password to Default": "Atur Ulang Kata Sandi ke Bawaan", + "Reset judge to Auto": "Atur ulang juri ke Otomatis", + "Reset time": "Waktu pengaturan ulang", + "Reset to Defaults": "Atur Ulang ke Bawaan", + "Reset to default": "Atur ulang ke bawaan", + "Resources": "Sumber Daya", + "Response": "Respons", + "Response Format": "Format Respons", + "Responses": "Respons", + "Responses API": "API Respons", + "Restart": "Mulai Ulang", + "Restore model": "Pulihkan model", + "Resume key": "Lanjutkan kunci", + "Retry": "Coba Lagi", + "Risk Notice": "Pemberitahuan Risiko", + "Roo AI Assistant": "Asisten AI Roo", + "Rotate providers across requests instead of strict fallback order.": "Rotasi penyedia melintasi permintaan alih-alih urutan fallback yang ketat.", + "Round Robin": "Putaran Bulat", + "Round Robin — rotate": "Putaran Bulat — rotasi", + "Round Robin — rotates models across requests to spread load": "Putaran Bulat — merotasi model melintasi permintaan untuk mendistribusikan beban", + "Route AI requests through subscription, cheap, and free tiers with auto-fallback. One endpoint for Claude, GPT, Gemini, and more.": "Rutekan permintaan AI melalui langganan, tier murah, dan gratis dengan fallback otomatis. Satu titik akhir untuk Claude, GPT, Gemini, dan lainnya.", + "Route Requests": "Rutekan Permintaan", + "Routing Strategy": "Strategi Rute", + "Rows:": "Baris:", + "Run": "Jalankan", + "Run npx command to start the server instantly": "Jalankan perintah npx untuk memulai server segera", + "Run this command in your terminal, then click": "Jalankan perintah ini di terminal Anda, lalu klik", + "Running": "Berjalan", + "Running on your machine": "Berjalan di mesin Anda", + "Runtime": "Runtime", + "SSE URL": "URL SSE", + "START HERE": "MULAI DI SINI", + "Save": "Simpan", + "Save Changes": "Simpan Perubahan", + "Save Config": "Simpan Konfigurasi", + "Save Mappings": "Simpan Pemetaan", + "Save auth mode": "Simpan mode autentikasi", + "Save current Base URL and API key as a browser-local preset": "Simpan URL Dasar dan kunci API saat ini sebagai preset lokal browser", + "Save this key now!": "Simpan kunci ini sekarang!", + "Saved": "Tersimpan", + "Saving": "Menyimpan", + "Saving...": "Menyimpan...", + "Scan QR to connect instantly": "Pindai QR untuk terhubung segera", + "Scopes": "Cakupan", + "Screen sharing": "Berbagi layar", + "Scroll down to": "Gulir ke bawah ke", + "Search by name or description...": "Cari berdasarkan nama atau deskripsi...", + "Search language...": "Cari bahasa...", + "Search model id": "Cari ID model", + "Search providers...": "Cari penyedia...", + "Search...": "Cari...", + "Security": "Keamanan", + "Security required: ": "Keamanan diperlukan: ", + "Security risk: no password set. You will be asked to set one when logging in remotely.": "Risiko keamanan: tidak ada kata sandi yang diatur. Anda akan diminta untuk mengatur satu saat login secara remote.", + "Select": "Pilih", + "Select All": "Pilih Semua", + "Select Cowork Model": "Pilih Model Cowork", + "Select Endpoint": "Pilih Titik Akhir", + "Select Judge Model": "Pilih Model Juri", + "Select Language": "Pilih Bahasa", + "Select Model": "Pilih Model", + "Select Model for Cline": "Pilih Model untuk Cline", + "Select Model for Codex": "Pilih Model untuk Codex", + "Select Model for DeepSeek TUI": "Pilih Model untuk DeepSeek TUI", + "Select Model for Factory Droid": "Pilih Model untuk Factory Droid", + "Select Model for GitHub Copilot": "Pilih Model untuk GitHub Copilot", + "Select Model for Hermes Agent": "Pilih Model untuk Hermes Agent", + "Select Model for Kilo Code": "Pilih Model untuk Kilo Code", + "Select Model for Open Claw": "Pilih Model untuk Open Claw", + "Select Model for OpenCode": "Pilih Model untuk OpenCode", + "Select Model for jcode": "Pilih Model untuk jcode", + "Select Provider": "Pilih Penyedia", + "Select Subagent Model for Codex": "Pilih Model Subagen untuk Codex", + "Select Subagent Model for OpenCode": "Pilih Model Subagen untuk OpenCode", + "Select a provider": "Pilih penyedia", + "Select all": "Pilih semua", + "Select language": "Pilih bahasa", + "Select models to add": "Pilih model untuk ditambahkan", + "Select one or more connections, then click Proxy Action.": "Pilih satu atau lebih koneksi, lalu klik Tindakan Proxy.", + "Select to pre-fill, then edit model ID in the input": "Pilih untuk mengisi sebelumnya, lalu edit ID model di input", + "Select your": "Pilih", + "Selected connections have mixed proxy bindings": "Koneksi yang dipilih memiliki ikatan proxy campuran", + "Selected only": "Hanya yang dipilih", + "Selected provider": "Penyedia yang dipilih", + "Selecting None will unbind selected connections from proxy pool.": "Memilih Tidak Ada akan melepaskan koneksi yang dipilih dari pool proxy.", + "Send": "Kirim", + "Send to Provider": "Kirim ke Penyedia", + "Sent to provider as:": "Dikirim ke penyedia sebagai:", + "Server": "Server", + "Server Disconnected": "Server Terputus", + "Server off": "Server mati", + "Server running on": "Server berjalan di", + "Service is running in terminal. You can close this web page. Shutdown will stop the service.": "Layanan sedang berjalan di terminal. Anda dapat menutup halaman web ini. Shutdown akan menghentikan layanan.", + "Set Password": "Atur Kata Sandi", + "Set a new password before accessing the dashboard remotely.": "Atur kata sandi baru sebelum mengakses dasbor secara remote.", + "Set password": "Atur kata sandi", + "Setting password for the first time. Leave current password empty or use default:": "Mengatur kata sandi untuk pertama kali. Kosongkan kata sandi saat ini atau gunakan bawaan:", + "Setting up": "Mengatur", + "Settings": "Pengaturan", + "Settings applied successfully!": "Pengaturan berhasil diterapkan!", + "Settings reset successfully!": "Pengaturan berhasil diatur ulang!", + "Setup": "Pengaturan", + "Setup + index of all capabilities. Start here — covers base URL, auth, model discovery, and links to every capability skill.": "Pengaturan + indeks semua kemampuan. Mulai di sini — mencakup URL dasar, autentikasi, penemuan model, dan tautan ke setiap keterampilan kemampuan.", + "Share Endpoint": "Bagikan Titik Akhir", + "Share URL with team members": "Bagikan URL dengan anggota tim", + "Show": "Tampilkan", + "Show all": "Tampilkan semua", + "Show key": "Tampilkan kunci", + "Show only selected models": "Tampilkan hanya model yang dipilih", + "Showing": "Menampilkan", + "Shutdown": "Matikan", + "Sign in with OIDC": "Masuk dengan OIDC", + "Simple chat interface to interact with any AI model from connected providers. Select a model and start chatting!": "Antarmuka obrolan sederhana untuk berinteraksi dengan model AI apa pun dari penyedia yang terhubung. Pilih model dan mulai mengobrol!", + "Single": "Tunggal", + "Single API endpoint for all major AI providers. Simplify your integration.": "Satu titik akhir API untuk semua penyedia AI utama. Sederhanakan integrasi Anda.", + "Some models are not responding": "Beberapa model tidak merespons", + "Sort Codex quotas by remaining": "Urutkan kuota Codex berdasarkan yang tersisa", + "Sort accounts by earliest quota reset time": "Urutkan akun berdasarkan waktu reset kuota paling awal", + "Source Body": "Badan Sumber", + "Sourcegraph Amp coding assistant CLI": "CLI asisten pemrograman Sourcegraph Amp", + "Special reasoning/thinking tokens (fallback to output rate)": "Token penalaran/pemikiran khusus (fallback ke tarif keluaran)", + "Speech To Text": "Suara ke Teks", + "Speech-to-Text": "Suara-ke-Teks", + "Standard prompt tokens": "Token prompt standar", + "Start DNS": "Mulai DNS", + "Start Date": "Tanggal Mulai", + "Start Free": "Mulai Gratis", + "Start Headroom": "Mulai Headroom", + "Start Headroom separately at the configured URL, then recheck.": "Mulai Headroom secara terpisah di URL yang dikonfigurasi, lalu periksa ulang.", + "Start MITM": "Mulai MITM", "Start Server": "Mulai Server", - "Enable DNS per tool below to activate interception": "Aktifkan DNS untuk setiap alat di bawah untuk mengaktifkan intersepsi", + "Start Tunnel": "Mulai Tunnel", + "Start a conversation": "Mulai percakapan", + "Starting 9Router...": "Memulai 9Router...", + "Status": "Status", + "Status:": "Status:", + "Step 1: Open this URL in your browser": "Langkah 1: Buka URL ini di browser Anda", + "Step 2: Paste the callback URL here": "Langkah 2: Tempel URL callback di sini", + "Sticky Limit": "Batas Lengket", + "Sticky:": "Lengket:", + "Stop": "Hentikan", + "Stop DNS": "Hentikan DNS", + "Stop Headroom": "Hentikan Headroom", + "Stop MITM": "Hentikan MITM", + "Stop Server": "Hentikan Server", + "Stopped": "Dihentikan", + "Strict Proxy": "Proxy Ketat", + "Style": "Gaya", + "Subagent Model": "Model Subagen", "Sudo Password Required": "Kata Sandi Sudo Diperlukan", - "Enter your sudo password to start/stop MITM server": "Masukkan kata sandi sudo Anda untuk memulai/menghentikan server MITM", - "Sudo Password": "Kata Sandi Sudo", - "Click to add, click again to remove. Changes are saved automatically.": "Klik untuk menambah, klik lagi untuk menghapus. Perubahan disimpan secara otomatis.", + "Sudo password is required": "Kata sandi sudo diperlukan", + "Suggested free models (≥200k context):": "Model gratis yang disarankan (≥200k konteks):", + "Suggested shorthand examples: g25p → gemini/gemini-2.5-pro, g25f → gemini/gemini-2.5-flash, cs45 → cc/claude-sonnet-4-5-20250929.": "Contoh nama pendek yang disarankan: g25p → gemini/gemini-2.5-pro, g25f → gemini/gemini-2.5-flash, cs45 → cc/claude-sonnet-4-5-20250929.", + "Support up to 20 active apps & 50 custom domains": "Mendukung hingga 20 aplikasi aktif & 50 domain kustom", + "Supported formats: protocol://user:pass@host:port, host:port:user:pass": "Format yang didukung: protocol://user:pass@host:port, host:port:user:pass", + "Sync settings across devices with optional cloud storage.": "Sinkronkan pengaturan lintas perangkat dengan penyimpanan cloud opsional.", + "System": "Sistem", + "TTFT:": "TTFT:", + "Tailscale": "Tailscale", + "Tailscale Funnel": "Tailscale Funnel", + "Tailscale Funnel will be stopped. Remote access via Tailscale URL will stop working.": "Tailscale Funnel akan dihentikan. Akses remote melalui Tailscale URL akan berhenti berfungsi.", + "Tailscale installed": "Tailscale terinstal", + "Tailscale is not installed. Install it to enable Funnel.": "Tailscale tidak terinstal. Instal untuk mengaktifkan Funnel.", + "Target Request": "Permintaan Target", + "Tavily / Exa / Brave / Serper / SearXNG / Google PSE / You.com.": "Tavily / Exa / Brave / Serper / SearXNG / Google PSE / You.com.", + "Temperature": "Temperatur", + "Terminal": "Terminal", + "Terms of Service": "Ketentuan Layanan", + "Terse-style system prompt → ~65% fewer output tokens (up to 87%)": "Prompt sistem gaya ringkas → ~65% lebih sedikit token keluaran (hingga 87%)", + "Test": "Uji", + "Test Again": "Uji Lagi", + "Test All": "Uji Semua", + "Test Example": "Contoh Uji", + "Test Results": "Hasil Pengujian", + "Test all API Key connections": "Uji semua koneksi Kunci API", + "Test all Compatible connections": "Uji semua koneksi Kompatibel", + "Test all Free connections": "Uji semua koneksi Gratis", + "Test all Free provider connections": "Uji semua koneksi penyedia Gratis", + "Test all OAuth connections": "Uji semua koneksi OAuth", + "Test connection": "Uji koneksi", + "Test model": "Uji model", + "Test proxy": "Uji proxy", + "Test proxy URL": "Uji URL Proxy", + "Testing...": "Menguji...", + "Text To Speech": "Teks ke Suara", + "Text To Speech combo": "Kombinasi Teks ke Suara", + "Text to Image": "Teks ke Gambar", + "Text to Image combo": "Kombinasi Teks ke Gambar", + "Text-to-Speech": "Teks-ke-Suara", + "Text-to-image via DALL-E, Imagen, FLUX, MiniMax, SDWebUI…": "Teks-ke-gambar melalui DALL-E, Imagen, FLUX, MiniMax, SDWebUI…", + "The Cloudflare tunnel will be disconnected. Remote access via tunnel URL will stop working.": "Tunnel Cloudflare akan terputus. Akses remote melalui tunnel URL akan berhenti berfungsi.", + "The proxy server has been stopped.": "Server proxy telah dihentikan.", + "The request is fulfilled by OpenAI, Anthropic, Gemini, or others instantly.": "Permintaan dipenuhi oleh OpenAI, Anthropic, Gemini, atau lainnya secara instan.", + "The tunnel will be disconnected. Remote access will stop working.": "Tunnel akan terputus. Akses remote akan berhenti berfungsi.", + "The unified endpoint for AI generation. Connect, route, and manage your AI providers with ease.": "Titik akhir terpadu untuk pembuatan AI. Hubungkan, rutekan, dan kelola penyedia AI Anda dengan mudah.", + "The unified interface for modern AI infrastructure": "Antarmuka terpadu untuk infrastruktur AI modern", + "The unified interface for modern AI infrastructure. Secure, observable, and scalable.": "Antarmuka terpadu untuk infrastruktur AI modern. Aman, dapat diamati, dan dapat diskalakan.", + "Theme": "Tema", + "Thinking": "Berpikir", + "Thinking Process": "Proses Berpikir", + "This is the only time you will see this key. Store it securely.": "Ini satu-satunya saat Anda akan melihat kunci ini. Simpan dengan aman.", + "This provider is ready to use.": "Penyedia ini siap digunakan.", + "This provider is ready to use. Optionally route requests through a proxy pool to bypass IP-based limits.": "Penyedia ini siap digunakan. Secara opsional rutekan permintaan melalui proxy pool untuk melewati batasan berbasis IP.", + "This value is write-only after saving.": "Nilai ini hanya dapat ditulis setelah disimpan.", + "Timestamp": "Cap Waktu", + "Timestamp:": "Cap Waktu:", + "To get a fresh API key, paste your browser cookie from": "Untuk mendapatkan kunci API baru, tempel cookie browser Anda dari", + "Today": "Hari Ini", + "Toggle DNS to redirect": "Alihkan DNS untuk mengalihkan", + "Toggle auto-ping": "Alihkan ping otomatis", + "Token Saver": "Penghemat Token", + "Token Types:": "Jenis Token:", + "Token auto-detected from Kiro IDE successfully!": "Token terdeteksi otomatis dari Kiro IDE berhasil!", + "Token is used once for deployment and not stored.": "Token digunakan sekali untuk deploy dan tidak disimpan.", + "Token is used once for deployment, not stored. Found in Organization Settings.": "Token digunakan sekali untuk deploy, tidak disimpan. Ditemukan di Pengaturan Organisasi.", + "Token will be auto-filled...": "Token akan diisi otomatis...", + "Tokens": "Token", + "Tokens auto-detected from Cursor IDE successfully!": "Token terdeteksi otomatis dari Cursor IDE berhasil!", + "Tokens used to create cache entries (fallback to input rate)": "Token yang digunakan untuk membuat entri cache (fallback ke tarif masukan)", + "Tomorrow": "Besok", + "Tool not found or disabled.": "Alat tidak ditemukan atau dinonaktifkan.", + "Tools": "Alat", + "Tools:": "Alat:", + "Total Cost": "Total Biaya", + "Total Input Tokens": "Total Token Masukan", + "Total Models": "Total Model", + "Total Requests": "Total Permintaan", + "Total Tokens": "Total Token", + "Total:": "Total:", + "Track and manage your API quota limits": "Lacak dan kelola batas kuota API Anda", + "Track token usage, costs, and performance across all providers.": "Lacak penggunaan token, biaya, dan kinerja di semua penyedia.", + "Transcribe audio via OpenAI Whisper, Groq, Gemini, Deepgram, AssemblyAI…": "Transkripsikan audio melalui OpenAI Whisper, Groq, Gemini, Deepgram, AssemblyAI…", + "Transferring data...": "Mentransfer data...", + "Translator": "Penerjemah", + "Translator Debug": "Debug Penerjemah", + "Tried in order (top-down) or rotated when round-robin is on.": "Dicoba secara berurutan (atas ke bawah) atau dirotasi saat putaran bulat aktif.", + "Trust Cert": "Percayai Sertifikat", + "Trusted": "Dipercaya", + "Try Again": "Coba Lagi", + "Tunnel": "Tunnel", + "Tunnel connected!": "Tunnel terhubung!", + "Tunnel disabled": "Tunnel dinonaktifkan", + "Turn off Empty": "Nonaktifkan Kosong", + "Turn on Available": "Aktifkan Tersedia", + "Turn request detail recording on/off globally": "Aktifkan/nonaktifkan pencatatan detail permintaan secara global", + "Twitter": "Twitter", + "URL → markdown / text / HTML via Firecrawl, Jina, Tavily, Exa.": "URL → markdown / text / HTML melalui Firecrawl, Jina, Tavily, Exa.", + "Unavailable": "Tidak Tersedia", + "Under": "Di bawah", + "Unified Endpoint": "Titik Akhir Terpadu", + "Unknown": "Tidak Diketahui", + "Unselect all": "Batalkan pilihan semua", + "Update": "Perbarui", + "Update 9Router": "Perbarui 9Router", + "Update Password": "Perbarui Kata Sandi", + "Update now": "Perbarui sekarang", + "Upstream Auth Error": "Kesalahan Autentikasi Hulu", + "Upstream Unavailable": "Hulu Tidak Tersedia", + "Usage": "Penggunaan", + "Usage & Analytics": "Penggunaan & Analitik", + "Usage / Limit": "Penggunaan / Batas", + "Usage Logs": "Log Penggunaan", + "Usage Tracking": "Pelacakan Penggunaan", + "Usage by API Key": "Penggunaan per Kunci API", + "Usage by Account": "Penggunaan per Akun", + "Usage by Endpoint": "Penggunaan per Titik Akhir", + "Usage by Model": "Penggunaan per Model", + "Usage:": "Penggunaan:", + "Use 9Router model aliases to keep Amp shorthand mappings stable across provider updates.": "Gunakan alias model 9Router untuk menjaga pemetaan nama pendek Amp tetap stabil selama pembaruan penyedia.", + "Use Antigravity IDE & GitHub Copilot → with ANY provider/model from 9Router": "Gunakan Antigravity IDE & GitHub Copilot → dengan PENYEDIA/model APA PUN dari 9Router", + "Use Authentik or any OIDC provider to sign in to the dashboard.": "Gunakan Authentik atau penyedia OIDC mana pun untuk masuk ke dasbor.", + "Use Authentik or any OIDC provider to sign in to the dashboard. You can enable password-only, OIDC-only, or both for the dashboard; model API access still uses API keys.": "Gunakan Authentik atau penyedia OIDC mana pun untuk masuk ke dasbor. Anda dapat mengaktifkan hanya kata sandi, hanya OIDC, atau keduanya untuk dasbor; akses model API tetap menggunakan kunci API.", + "Use a GitLab OAuth application": "Gunakan aplikasi OAuth GitLab", + "Use a GitLab PAT with api scope": "Gunakan PAT GitLab dengan cakupan api", + "Use a direct xAI API key from console.x.ai. This is separate from Grok Build OAuth.": "Gunakan kunci API xAI langsung dari console.x.ai. Ini terpisah dari Grok Build OAuth.", + "Use a local proxy for Start/Stop, or an external Docker sidecar like http://headroom:8787.": "Gunakan proxy lokal untuk Mulai/Henti, atau sidecar Docker eksternal seperti http://headroom:8787.", + "Use a long-lived Kiro/CodeWhisperer API key (headless auth).": "Gunakan kunci API Kiro/CodeWhisperer jangka panjang (autentikasi headless).", + "Use in Cursor/Cline": "Gunakan di Cursor/Cline", + "Use the buttons above to add OpenAI or Anthropic compatible endpoints": "Gunakan tombol di atas untuk menambahkan titik akhir kompatibel OpenAI atau Anthropic", + "Use your API from any network": "Gunakan API Anda dari jaringan mana pun", + "Valid": "Valid", + "Vectors for RAG / semantic search via OpenAI, Gemini, Mistral…": "Vektor untuk RAG / pencarian semantik melalui OpenAI, Gemini, Mistral…", + "Vercel API Token": "Token API Vercel", + "Vercel Relay": "Relay Vercel", + "Vercel serves millions of apps — providers can't block Vercel IPs without affecting legitimate traffic": "Vercel melayani jutaan aplikasi — penyedia tidak dapat memblokir IP Vercel tanpa mempengaruhi lalu lintas yang sah", + "Verification URL": "Verifikasi URL", + "Video": "Video", + "View Codex reset credit expiry": "Lihat kedaluwarsa kredit reset Codex", + "View Full Details": "Lihat Detail Lengkap", + "View on GitHub": "Lihat di GitHub", + "Visit the URL below and enter the code:": "Kunjungi URL di bawah dan masukkan kode:", + "Visit the login URL below and authorize:": "Kunjungi URL login di bawah dan otorisasi:", + "Voice": "Suara", + "Voice ID": "ID Suara", + "Voyage AI": "Voyage AI", + "Waiting for Authorization": "Menunggu Otorisasi", + "Waiting for authorization...": "Menunggu otorisasi...", + "Warning": "Peringatan", + "Web Fetch": "Pengambilan Web", + "Web Fetch & Search": "Pengambilan & Pencarian Web", + "Web Search": "Pencarian Web", + "Web Search & Fetch (Exa)": "Pencarian & Pengambilan Web (Exa)", + "Welcome": "Selamat Datang", + "What is Cloudflare Relay?": "Apa itu Cloudflare Relay?", + "What is Deno Relay?": "Apa itu Deno Relay?", + "What is Vercel Relay?": "Apa itu Vercel Relay?", + "When": "Kapan", + "When ON, dashboard requires password. When OFF, access without login.": "Ketika AKTIF, dasbor memerlukan kata sandi. Ketika NONAKTIF, akses tanpa login.", + "Windows:": "Windows:", + "Windows: Run 9Router terminal as Administrator": "Windows: Jalankan terminal 9Router sebagai Administrator", + "Windows: Run terminal (9Router) as Administrator to enable MITM": "Windows: Jalankan terminal (9Router) sebagai Administrator untuk mengaktifkan MITM", + "Worker Name": "Nama Worker", + "Works on any device": "Berfungsi di perangkat apa pun", + "Writes to": "Menulis ke", + "You can override default pricing for specific models. Reset to defaults anytime to restore standard rates.": "Anda dapat mengganti harga bawaan untuk model tertentu. Atur ulang ke bawaan kapan saja untuk memulihkan tarif standar.", + "Your": "Anda", + "Your Account Name": "Nama Akun Anda", + "Your Code": "Kode Anda", + "Your Kiro account via": "Akun Kiro Anda melalui", + "Your OAuth application client ID": "ID klien aplikasi OAuth Anda", + "Your organization's AWS IAM Identity Center URL": "URL Pusat Identitas AWS IAM organisasi Anda", + "Your requests start from your favorite tools or our unified SDK. Just change the base URL.": "Permintaan Anda dimulai dari alat favorit Anda atau SDK terpadu kami. Cukup ubah URL dasar.", + "Your requests start from your favorite tools — Cursor, Claude, Copilot, or any OpenAI-compatible SDK.": "Permintaan Anda dimulai dari alat favorit Anda — Cursor, Claude, Copilot, atau SDK kompatibel OpenAI apa pun.", + "account has been connected.": "akun telah terhubung.", + "active": "aktif", + "add OpenAI/Anthropic compatible endpoints": "tambahkan titik akhir kompatibel OpenAI/Anthropic", + "added)": "ditambahkan)", + "again after install.": "lagi setelah instalasi.", + "and click": "dan klik", + "apiKey": "apiKey", + "below.": "di bawah.", + "bound": "terikat", + "chars)": "karakter)", + "cloudflare relay": "relay cloudflare", + "connection": "koneksi", + "connections": "koneksi", + "daily-cloudcode-pa.googleapis.com": "daily-cloudcode-pa.googleapis.com", + "dark": "gelap", + "disabled": "dinonaktifkan", + "dollars per million tokens": "dolar per juta token", + "e.g. CwhRBWXzGAHq8TQ4Fs17": "mis. CwhRBWXzGAHq8TQ4Fs17", + "e.g. a warm, gentle voice, speaking slowly with a British accent": "mis.: suara hangat dan lembut, berbicara pelan dengan aksen Inggris", + "e.g. claude-opus-4-5": "mis. claude-opus-4-5", + "e.g. my-model-id": "mis. my-model-id", + "e.g. tts-1-hd": "mis. tts-1-hd", + "e.g. voyage-3, embed-english-v3.0, text-embedding-3-small": "mis. voyage-3, embed-english-v3.0, text-embedding-3-small", + "e.g., Production API, Dev Environment": "mis., API Produksi, Lingkungan Pengembangan", + "every request bills all panel models + the judge": "setiap permintaan menagih semua model panel + juri", + "export": "ekspor", + "failed": "gagal", + "git/grep/ls/tree/logs → 60-90% fewer input tokens": "git/grep/ls/tree/logs → 60-90% lebih sedikit token masukan", + "h ago": "jam yang lalu", + "has been connected.": "telah terhubung.", + "iFlow AI": "iFlow AI", + "iFlow Cookie Authentication": "Autentikasi Cookie iFlow", + "import": "impor", + "inactive": "nonaktif", + "jcode - Manual Configuration": "jcode - Konfigurasi Manual", + "jcode CLI not detected locally": "jcode CLI tidak terdeteksi secara lokal", + "jcode is a Rust-based coding agent with semantic memory, multi-agent swarms, and extreme performance (27.8 MB RAM, 14ms boot).": "jcode adalah agen pemrograman berbasis Rust dengan memori semantik, kelompok multi-agen, dan kinerja ekstrem (27.8 MB RAM, 14ms boot).", + "kiro://kiro.kiroAgent/authenticate-success?code=...": "kiro://kiro.kiroAgent/authenticate-success?code=...", + "light": "terang", + "m ago": "menit yang lalu", + "macOS / Linux / Windows:": "macOS / Linux / Windows:", + "macOS / Linux:": "macOS / Linux:", + "macOS/Linux:": "macOS/Linux:", + "more": "lebih banyak", + "more providers": "lebih banyak penyedia", + "ms / Total": "ms / Total", + "name|apiKey": "name|apiKey", + "no_proxy:": "no_proxy:", + "not detected locally": "tidak terdeteksi secara lokal", + "npm install -g 9router": "npm install -g 9router", + "npx 9router": "npx 9router", + "open http://localhost:9099": "open http://localhost:9099", + "openid profile email": "openid profile email", + "optional context to improve accuracy": "konteks opsional untuk meningkatkan akurasi", + "or VS Code extension marketplace.": "atau marketplace ekstensi VS Code.", + "or just": "atau cukup", + "passed": "lulus", + "platform.iflow.cn": "platform.iflow.cn", + "queries all models in parallel, then a judge synthesizes one answer. Best quality, but costs the most: every request bills all panel models + the judge (N+1 calls)": "menanyakan semua model secara paralel, lalu juri mensintesis satu jawaban. Kualitas terbaik, tetapi biaya paling mahal: setiap permintaan menagih semua model panel + juri (N+1 panggilan)", + "records, batches every": "rekaman, setiap batch", + "requests, max": "permintaan, maks", + "rotates models across requests to spread load": "merotasi model melintasi permintaan untuk mendistribusikan beban", + "s)": "d)", + "s...": "d...", + "seconds...": "detik...", + "sends image/PDF/audio requests to a model that supports them first": "mengirim permintaan gambar/PDF/audio ke model yang mendukungnya terlebih dahulu", + "sk-...": "sk-...", + "sk_9router (default)": "sk_9router (bawaan)", + "system": "ikuti sistem", + "tested": "diuji", + "the database.": "database.", + "to apply changes": "untuk menerapkan perubahan", + "to verify.": "untuk memverifikasi.", + "traffic through 9Router via MITM.": "lalu lintas melalui 9Router via MITM.", + "tries models in order (next on failure)": "mencoba model secara berurutan (berikutnya saat gagal)", + "unknown": "tidak diketahui", + "v1.0 is now live": "v1.0 sudah aktif", + "vercel relay": "relay vercel", + "yet.": ".", + "your-org.deno.net": "your-org.deno.net", + "© 2025 9Router. All rights reserved.": "© 2025 9Router. Hak cipta dilindungi.", + "— queries all models in parallel, then a judge synthesizes one answer. Best quality, but costs the most: every request bills all panel models + the judge (N+1 calls)": "— menanyakan semua model secara paralel, lalu juri mensintesis satu jawaban. Kualitas terbaik, tetapi biaya paling mahal: setiap permintaan menagih semua model panel + juri (N+1 panggilan)", + "— rotates models across requests to spread load": "— merotasi model melintasi permintaan untuk mendistribusikan beban", + "— sends image/PDF/audio requests to a model that supports them first": "— mengirim permintaan gambar/PDF/audio ke model yang mendukungnya terlebih dahulu", + "— tries models in order (next on failure)": "— mencoba model secara berurutan (berikutnya saat gagal)", + "→ OpenAI": "→ OpenAI", + "→ Target": "→ Target", + "→ localhost": "→ localhost", + "⚠️ Enable DNS to edit model mappings": "⚠️ Aktifkan DNS untuk mengedit pemetaan model", + "⚠️ Local plugins run as subprocess via": "⚠️ Plugin lokal berjalan sebagai subproses melalui", + "⚠️ MITM intercepts HTTPS traffic of IDE tools (Antigravity, GitHub Copilot, Kiro) via local CA to redirect requests to your providers. May violate ToS → account ban. Use at your own risk.": "⚠️ MITM mencegat lalu lintas HTTPS alat IDE (Antigravity, GitHub Copilot, Kiro) melalui CA lokal untuk mengalihkan permintaan ke penyedia Anda. Mungkin melanggar ToS → risiko pemblokiran akun. Gunakan dengan risiko Anda sendiri.", "⚠️ Risk Notice: This provider uses a subscription/OAuth session not officially licensed for proxy/router use. Account may be restricted or banned. Use at your own risk.": "⚠️ Pemberitahuan Risiko: Penyedia ini menggunakan sesi langganan/OAuth yang tidak dilisensikan secara resmi untuk penggunaan proxy/router. Akun mungkin dibatasi atau diblokir. Gunakan dengan risiko Anda sendiri.", - "⚠️ MITM intercepts HTTPS traffic of IDE tools (Antigravity, GitHub Copilot, Kiro) via local CA to redirect requests to your providers. May violate ToS → account ban. Use at your own risk.": "⚠️ MITM mencegat lalu lintas HTTPS alat IDE (Antigravity, GitHub Copilot, Kiro) melalui CA lokal untuk mengalihkan permintaan ke penyedia Anda. Mungkin melanggar ToS → risiko ban akun. Gunakan dengan risiko Anda sendiri.", - "Endpoint is exposed without an API key.": "Endpoint terekspos tanpa kunci API." + "✓ Confirm Add": "✓ Konfirmasi Tambah", + "📝 Configure providers in dashboard or use environment variables": "📝 Konfigurasi penyedia di dasbor atau gunakan variabel lingkungan", + "🔐 OAuth required. Add now and authenticate after Apply; tool list will be discovered after first connect.": "🔐 OAuth diperlukan. Tambah sekarang dan autentikasi setelah Terapkan; daftar alat akan ditemukan setelah koneksi pertama." } diff --git a/public/i18n/literals/zh-CN.json b/public/i18n/literals/zh-CN.json index da045742..5de38ff0 100644 --- a/public/i18n/literals/zh-CN.json +++ b/public/i18n/literals/zh-CN.json @@ -235,6 +235,7 @@ "Codex CLI not installed": "Codex CLI 未安装", "Codex Reset Credit Expiry": "Codex 重置信用有效期", "Codex uses": "Codex 使用", + "Combo & Vision Adapter": "组合与视觉适配器", "Combo Name": "组合名称", "Combo Round Robin": "组合轮询", "Combo Sticky Limit": "组合粘性限制", diff --git a/public/providers/alims-intl.png b/public/providers/alims-intl.png new file mode 100644 index 00000000..9bee7d5b Binary files /dev/null and b/public/providers/alims-intl.png differ diff --git a/public/providers/alitp-intl.png b/public/providers/alitp-intl.png new file mode 100644 index 00000000..9bee7d5b Binary files /dev/null and b/public/providers/alitp-intl.png differ diff --git a/public/providers/fish-audio.png b/public/providers/fish-audio.png new file mode 100644 index 00000000..fa8f9541 Binary files /dev/null and b/public/providers/fish-audio.png differ diff --git a/public/providers/selfhosted-embedding.png b/public/providers/selfhosted-embedding.png new file mode 100644 index 00000000..201cb10e Binary files /dev/null and b/public/providers/selfhosted-embedding.png differ diff --git a/public/providers/selfhosted-stt.png b/public/providers/selfhosted-stt.png new file mode 100644 index 00000000..201cb10e Binary files /dev/null and b/public/providers/selfhosted-stt.png differ diff --git a/public/providers/selfhosted-tts.png b/public/providers/selfhosted-tts.png new file mode 100644 index 00000000..201cb10e Binary files /dev/null and b/public/providers/selfhosted-tts.png differ diff --git a/skills/9router-web-fetch/SKILL.md b/skills/9router-web-fetch/SKILL.md index 69c41ac3..c9c5af1b 100644 --- a/skills/9router-web-fetch/SKILL.md +++ b/skills/9router-web-fetch/SKILL.md @@ -1,6 +1,6 @@ --- name: 9router-web-fetch -description: Fetch URL → markdown / text / HTML via 9Router /v1/web/fetch using Firecrawl / Jina Reader / Tavily Extract / Exa Contents. Use when the user wants to scrape a webpage, extract URL content, read article, or convert a URL to markdown. +description: Fetch URL → markdown / text / HTML via 9Router /v1/web/fetch using Ollama Cloud / Firecrawl / Jina Reader / Tavily Extract / Exa Contents. Use when the user wants to scrape a webpage, extract URL content, read article, or convert a URL to markdown. --- # 9Router — Web Fetch @@ -62,6 +62,17 @@ curl -X POST $NINEROUTER_URL/v1/web/fetch \ -d '{"model":"tavily","url":"https://example.com","format":"markdown","max_characters":0}' ``` +### Ollama Cloud + +Uses the API key from the existing `ollama` connection. + +```bash +curl -X POST $NINEROUTER_URL/v1/web/fetch \ + -H "Authorization: Bearer $NINEROUTER_KEY" \ + -H "Content-Type: application/json" \ + -d '{"model":"ollama","url":"https://example.com","format":"markdown"}' +``` + JS: @@ -83,12 +94,15 @@ console.log(data.title, data.content.length); "url": "...", "title": "...", "content": { "format": "markdown", "text": "...", "length": 1234 }, + "links": ["https://example.com/related"], "metadata": { "author": null, "published_at": null, "language": null }, "usage": { "fetch_cost_usd": 0 }, "metrics": { "response_time_ms": 850, "upstream_latency_ms": 700 } } ``` +`links` is included when the upstream provider returns discovered page links (currently Ollama Cloud). + ## Provider quirks | Provider | Auth | Best for | @@ -97,3 +111,4 @@ console.log(data.title, data.content.length); | `jina-reader` | Bearer (optional) | Free tier (~1M chars/mo); fastest plain markdown | | `tavily` | Bearer | Bulk extract; returns `raw_content` | | `exa` | `x-api-key` | Pre-indexed pages; fast text extraction | +| `ollama` | Bearer | Markdown plus page title and discovered links; uses the Ollama Cloud key | diff --git a/src/app/(dashboard)/dashboard/cli-tools/components/ApiKeySelect.js b/src/app/(dashboard)/dashboard/cli-tools/components/ApiKeySelect.js index e0f24f36..60245e31 100644 --- a/src/app/(dashboard)/dashboard/cli-tools/components/ApiKeySelect.js +++ b/src/app/(dashboard)/dashboard/cli-tools/components/ApiKeySelect.js @@ -1,38 +1,76 @@ "use client"; -import { useState } from "react"; +import { useEffect, useMemo, useState } from "react"; +import { readKeyPresets, upsertKeyPreset, deleteKeyPreset, subscribeKeyPresets } from "./cliEndpointPresets"; const CUSTOM_VALUE = "__custom__"; +const SAVE_VALUE = "__save_key__"; export default function ApiKeySelect({ value, onChange, apiKeys = [], cloudEnabled = false, className = "" }) { - const isCustom = !apiKeys.some((k) => k.key === value) && value !== ""; - const [mode, setMode] = useState(() => { - if (!value) return apiKeys.length > 0 ? apiKeys[0].key : CUSTOM_VALUE; - if (apiKeys.some((k) => k.key === value)) return value; - return CUSTOM_VALUE; - }); - const [customInput, setCustomInput] = useState(isCustom ? value : ""); + const [savedKeys, setSavedKeys] = useState([]); + // Custom mode is sticky once the user types, so an emptied input doesn't jump back to a dropdown option + const [customMode, setCustomMode] = useState(false); + const [customInput, setCustomInput] = useState(""); + + useEffect(() => { + const sync = () => setSavedKeys(readKeyPresets()); + sync(); + return subscribeKeyPresets(sync); + }, []); + + const options = useMemo( + () => [ + ...apiKeys.map((k) => ({ value: k.key, label: k.key })), + ...savedKeys.map((p) => ({ value: `saved:${p.name}`, label: p.key, url: p.key, saved: true })), + { value: CUSTOM_VALUE, label: "Custom...", url: "" }, + ], + [apiKeys, savedKeys] + ); + + // Derive the active option from value — no sync effects needed when the parent updates it + const matched = value ? options.find((o) => o.value === value || o.url === value) : null; + const mode = matched ? matched.value : (customMode || value ? CUSTOM_VALUE : (options[0]?.value ?? CUSTOM_VALUE)); + const inputValue = customMode ? customInput : (value || ""); + const isSaved = typeof mode === "string" && mode.startsWith("saved:"); + const isCustom = mode === CUSTOM_VALUE; + const canSave = isCustom && (value || "").trim().length > 0 && !apiKeys.some((k) => k.key === value); + const noKeys = apiKeys.length === 0 && savedKeys.length === 0 && !customMode && !value; const handleSelect = (e) => { const next = e.target.value; - setMode(next); + if (next === SAVE_VALUE) { + upsertKeyPreset((value || "").trim()); + return; + } if (next === CUSTOM_VALUE) { + setCustomMode(true); setCustomInput(""); onChange(""); - } else { - onChange(next); + return; } + setCustomMode(false); + setCustomInput(""); + const opt = options.find((o) => o.value === next); + if (opt) onChange(opt.url ?? opt.value); }; const handleCustomInput = (e) => { const v = e.target.value; + setCustomMode(true); setCustomInput(v); onChange(v); }; - const noKeys = apiKeys.length === 0 && mode !== CUSTOM_VALUE; + const handleDeleteSaved = () => { + if (!isSaved) return; + deleteKeyPreset(mode.slice(6)); + setCustomMode(false); + setCustomInput(""); + const fallback = options.find((o) => o.value !== CUSTOM_VALUE && o.value !== mode); + onChange(fallback ? (fallback.url ?? fallback.value) : ""); + }; - if (noKeys && mode !== CUSTOM_VALUE) { + if (noKeys) { return ( {cloudEnabled ? "No API keys - Create one in Keys page" : "sk_9router (default)"} @@ -42,20 +80,27 @@ export default function ApiKeySelect({ value, onChange, apiKeys = [], cloudEnabl return (
- - {mode === CUSTOM_VALUE && ( +
+ + {isSaved && ( + + )} +
+ {isCustom && ( k.key === tokenFromFile)) { + if (tokenFromFile) { setSelectedApiKey(tokenFromFile); } } diff --git a/src/app/(dashboard)/dashboard/cli-tools/components/cliEndpointPresets.js b/src/app/(dashboard)/dashboard/cli-tools/components/cliEndpointPresets.js index 53cd0d43..beb78855 100644 --- a/src/app/(dashboard)/dashboard/cli-tools/components/cliEndpointPresets.js +++ b/src/app/(dashboard)/dashboard/cli-tools/components/cliEndpointPresets.js @@ -1,55 +1,79 @@ import { UPDATER_CONFIG } from "@/shared/constants/config"; -// Browser-local endpoint presets shared by every CLI tool card -const STORAGE_KEY = "9router.cliToolEndpointPresets"; -const CHANGE_EVENT = "9router:endpoint-presets-changed"; +// Browser-local preset stores (endpoints, API keys) shared by every CLI tool card +function createStore({ storageKey, changeEvent, itemField, normalize = (v) => v, defaultName = (v) => v }) { + const read = () => { + if (typeof window === "undefined") return []; + try { + const raw = JSON.parse(window.localStorage.getItem(storageKey) || "[]"); + if (!Array.isArray(raw)) return []; + return raw.filter((p) => p?.name && p?.[itemField]); + } catch { + return []; + } + }; + + const write = (items) => { + if (typeof window === "undefined") return; + window.localStorage.setItem(storageKey, JSON.stringify(items)); + window.dispatchEvent(new CustomEvent(changeEvent)); + }; + + return { + read, + subscribe: (handler) => { + if (typeof window === "undefined") return () => {}; + window.addEventListener(changeEvent, handler); + return () => window.removeEventListener(changeEvent, handler); + }, + // Adds or replaces a preset; returns the stored name, or null when skipped + upsert: (value, name) => { + const v = normalize(value); + if (!v) return null; + + const items = read(); + const existing = items.find((p) => normalize(p[itemField]) === v); + if (existing && !name) return existing.name; + + const finalName = (name || defaultName(v)).trim(); + if (!finalName) return null; + + const next = [...items.filter((p) => p.name !== finalName && normalize(p[itemField]) !== v), { name: finalName, [itemField]: v }] + .sort((a, b) => a.name.localeCompare(b.name)); + write(next); + return finalName; + }, + remove: (name) => write(read().filter((p) => p.name !== name)), + }; +} const stripSlash = (url) => (url || "").replace(/\/+$/, ""); -export function readPresets() { - if (typeof window === "undefined") return []; - try { - const raw = JSON.parse(window.localStorage.getItem(STORAGE_KEY) || "[]"); - if (!Array.isArray(raw)) return []; - return raw.filter((p) => p?.name && p?.baseUrl); - } catch { - return []; - } -} +const endpoints = createStore({ + storageKey: "9router.cliToolEndpointPresets", + changeEvent: "9router:endpoint-presets-changed", + itemField: "baseUrl", + normalize: stripSlash, + defaultName: (url) => { + try { return new URL(url).host; } catch { return url; } + }, +}); -function writePresets(presets) { - if (typeof window === "undefined") return; - window.localStorage.setItem(STORAGE_KEY, JSON.stringify(presets)); - window.dispatchEvent(new CustomEvent(CHANGE_EVENT)); -} +const apiKeys = createStore({ + storageKey: "9router.cliToolApiKeyPresets", + changeEvent: "9router:api-key-presets-changed", + itemField: "key", +}); -export function subscribePresets(handler) { - if (typeof window === "undefined") return () => {}; - window.addEventListener(CHANGE_EVENT, handler); - return () => window.removeEventListener(CHANGE_EVENT, handler); -} +export const readPresets = endpoints.read; +export const subscribePresets = endpoints.subscribe; +export const upsertPreset = endpoints.upsert; +export const deletePreset = endpoints.remove; -function defaultNameFor(url) { - try { return new URL(url).host; } catch { return url; } -} - -// Adds or replaces a preset; returns the stored name, or null when skipped -export function upsertPreset(baseUrl, name) { - const url = stripSlash(baseUrl); - if (!url) return null; - - const presets = readPresets(); - const existing = presets.find((p) => stripSlash(p.baseUrl) === url); - if (existing && !name) return existing.name; - - const finalName = (name || defaultNameFor(url)).trim(); - if (!finalName) return null; - - const next = [...presets.filter((p) => p.name !== finalName && stripSlash(p.baseUrl) !== url), { name: finalName, baseUrl: url }] - .sort((a, b) => a.name.localeCompare(b.name)); - writePresets(next); - return finalName; -} +export const readKeyPresets = apiKeys.read; +export const subscribeKeyPresets = apiKeys.subscribe; +export const upsertKeyPreset = apiKeys.upsert; +export const deleteKeyPreset = apiKeys.remove; // Save an applied endpoint unless it exactly matches a built-in dropdown option export function rememberEndpoint(baseUrl, { tunnelPublicUrl, tailscaleUrl, cloudUrl } = {}) { @@ -64,8 +88,4 @@ export function rememberEndpoint(baseUrl, { tunnelPublicUrl, tailscaleUrl, cloud return upsertPreset(url); } -export function deletePreset(name) { - writePresets(readPresets().filter((p) => p.name !== name)); -} - export { stripSlash }; diff --git a/src/app/(dashboard)/dashboard/providers/[id]/AddCustomModelModal.js b/src/app/(dashboard)/dashboard/providers/[id]/AddCustomModelModal.js index d0bf1659..3c9b21d0 100644 --- a/src/app/(dashboard)/dashboard/providers/[id]/AddCustomModelModal.js +++ b/src/app/(dashboard)/dashboard/providers/[id]/AddCustomModelModal.js @@ -2,17 +2,21 @@ import { useState, useEffect } from "react"; import PropTypes from "prop-types"; -import { Button, Modal } from "@/shared/components"; +import { Button, Modal, Toggle } from "@/shared/components"; +import { CAPACITY_META } from "@/shared/constants/models"; + +const defaultCaps = () => Object.fromEntries(Object.keys(CAPACITY_META).map((key) => [key, false])); export default function AddCustomModelModal({ isOpen, providerAlias, providerDisplayAlias, onSave, onClose }) { const [modelId, setModelId] = useState(""); + const [caps, setCaps] = useState(defaultCaps); const [testStatus, setTestStatus] = useState(null); // null | "testing" | "ok" | "error" const [testError, setTestError] = useState(""); const [saving, setSaving] = useState(false); // Reset state when modal opens useEffect(() => { - if (isOpen) { setModelId(""); setTestStatus(null); setTestError(""); } + if (isOpen) { setModelId(""); setCaps(defaultCaps()); setTestStatus(null); setTestError(""); } }, [isOpen]); // Strip provider's own alias prefix (e.g. "cc/model" -> "model" for cc provider) @@ -46,7 +50,7 @@ export default function AddCustomModelModal({ isOpen, providerAlias, providerDis if (!cleanId || saving) return; setSaving(true); try { - await onSave(cleanId); + await onSave(cleanId, caps); } finally { setSaving(false); } @@ -86,6 +90,22 @@ export default function AddCustomModelModal({ isOpen, providerAlias, providerDis

+
+ +
+ {Object.entries(CAPACITY_META).map(([key, meta]) => ( + setCaps((prev) => ({ ...prev, [key]: v }))} + label={meta.label} + description={meta.desc} + size="sm" + /> + ))} +
+
+ {/* Test result */} {testStatus === "ok" && (
diff --git a/src/app/(dashboard)/dashboard/providers/[id]/page.js b/src/app/(dashboard)/dashboard/providers/[id]/page.js index 14ac7b3e..8b0b7eb7 100644 --- a/src/app/(dashboard)/dashboard/providers/[id]/page.js +++ b/src/app/(dashboard)/dashboard/providers/[id]/page.js @@ -588,12 +588,12 @@ export default function ProviderDetailPage() { } }; - const handleAddCustomModel = async (modelId, type = "llm", providerAliasOverride = providerStorageAlias) => { + const handleAddCustomModel = async (modelId, type = "llm", providerAliasOverride = providerStorageAlias, caps) => { try { const res = await fetch("/api/models/custom", { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ providerAlias: providerAliasOverride, id: modelId, type }), + body: JSON.stringify({ providerAlias: providerAliasOverride, id: modelId, type, ...(caps ? { caps } : {}) }), }); if (res.ok) { await fetchCustomModels(); @@ -1006,7 +1006,7 @@ export default function ProviderDetailPage() { const isSelected = (connectionId) => selectedConnectionIds.includes(connectionId); const connectionsList = ( -
+
{connections .map((conn, index) => (
@@ -1876,8 +1876,8 @@ export default function ProviderDetailPage() { isOpen={showAddCustomModel} providerAlias={providerStorageAlias} providerDisplayAlias={providerDisplayAlias} - onSave={async (modelId) => { - await handleAddCustomModel(modelId, "llm", providerStorageAlias); + onSave={async (modelId, caps) => { + await handleAddCustomModel(modelId, "llm", providerStorageAlias, caps); setShowAddCustomModel(false); }} onClose={() => setShowAddCustomModel(false)} diff --git a/src/app/(dashboard)/dashboard/providers/components/ConnectionsCard.js b/src/app/(dashboard)/dashboard/providers/components/ConnectionsCard.js index 6b178bdd..d4533264 100644 --- a/src/app/(dashboard)/dashboard/providers/components/ConnectionsCard.js +++ b/src/app/(dashboard)/dashboard/providers/components/ConnectionsCard.js @@ -434,7 +434,7 @@ export default function ConnectionsCard({ providerId, isOAuth }) {
) : ( <> -
+
{connections.map((conn, idx) => ( s.query); const registerSearch = useHeaderSearchStore((s) => s.register); @@ -212,6 +214,9 @@ export default function ProvidersPage() { return { connected, error, total, errorCode, errorTime, allDisabled }; }; + const matchStatus = (stats, isNoAuth) => + matchesStatusFilter(statusFilter, stats, isNoAuth); + // Toggle all connections for a provider on/off. authType may be a single // string or an array (kiro counts oauth + api_key/apikey together). const handleToggleProvider = async (providerId, authType, newActive) => { @@ -267,7 +272,9 @@ export default function ProvidersPage() { textIcon: "OC", apiType: node.apiType, })) - .filter((p) => matchSearch(p.name)); + .filter( + (p) => matchSearch(p.name) && matchStatus(getProviderStats(p.id, "apikey")), + ); const anthropicCompatibleProviders = providerNodes .filter((node) => node.type === "anthropic-compatible") @@ -277,7 +284,9 @@ export default function ProvidersPage() { color: "#D97757", textIcon: "AC", })) - .filter((p) => matchSearch(p.name)); + .filter( + (p) => matchSearch(p.name) && matchStatus(getProviderStats(p.id, "apikey")), + ); // Dual-auth providers (oauth + apikey) store API keys as authType "apikey" // (and sometimes "api_key"). Card stats must count both so totals match detail. @@ -298,21 +307,32 @@ export default function ProvidersPage() { }; const oauthEntries = sortByPriority( - Object.entries(OAUTH_PROVIDERS).filter(([, info]) => !info.hidden && matchSearch(info.name)), + Object.entries(OAUTH_PROVIDERS).filter( + ([key, info]) => + !info.hidden && + matchSearch(info.name) && + matchStatus(getProviderStats(key, dualAuthTypes(info, key)), info.noAuth), + ), "oauth", ); const freeEntries = Object.entries(FREE_PROVIDERS) - .filter(([, info]) => !info.hidden && matchSearch(info.name)) + .filter( + ([key, info]) => + !info.hidden && + matchSearch(info.name) && + matchStatus(getProviderStats(key, dualAuthTypes(info, key)), info.noAuth), + ) .sort(([, a], [, b]) => (b.noAuth ? 1 : 0) - (a.noAuth ? 1 : 0)); // Free Tier cards may be oauth-only (e.g. kimchi) or dual-auth, so count via // dualAuthTypes per provider instead of a fixed "apikey" — otherwise oauth // connections are invisible here (mismatch with the detail page). const freeTierEntries = Object.entries(FREE_TIER_PROVIDERS) .filter( - ([, info]) => + ([key, info]) => !info.hidden && matchSearch(info.name) && - (info.serviceKinds ?? ["llm"]).includes("llm"), + (info.serviceKinds ?? ["llm"]).includes("llm") && + matchStatus(getProviderStats(key, dualAuthTypes(info, key)), info.noAuth), ) .sort(([ka, a], [kb, b]) => { const pa = a.priority ?? 999; @@ -328,10 +348,11 @@ export default function ProvidersPage() { // API Key: connected providers first, then alphabetical by name const apikeyEntries = Object.entries(APIKEY_PROVIDERS) .filter( - ([, info]) => + ([key, info]) => !info.hidden && (info.serviceKinds ?? ["llm"]).includes("llm") && - matchSearch(info.name), + matchSearch(info.name) && + matchStatus(getProviderStats(key, "apikey"), info.noAuth), ) .sort(([ka, a], [kb, b]) => { const ca = getProviderStats(ka, "apikey").total > 0 ? 0 : 1; @@ -339,7 +360,7 @@ export default function ProvidersPage() { if (ca !== cb) return ca - cb; return (a.name || "").localeCompare(b.name || ""); }); - const isApikeySearching = !!searchQuery.trim(); + const isApikeySearching = !!searchQuery.trim() || statusFilter !== "all"; const visibleApikeyEntries = isApikeySearching || showAllApikey ? apikeyEntries @@ -365,12 +386,29 @@ export default function ProvidersPage() { return (
+
+ +
+ {!hasAnyResult && (
search_off -

No providers match your search

+

+ No providers match your search or filters +

)} diff --git a/src/app/(dashboard)/dashboard/providers/utils.js b/src/app/(dashboard)/dashboard/providers/utils.js new file mode 100644 index 00000000..fa114325 --- /dev/null +++ b/src/app/(dashboard)/dashboard/providers/utils.js @@ -0,0 +1,19 @@ +export const STATUS_FILTER_OPTIONS = [ + { value: "all", label: "All" }, + { value: "active", label: "Active" }, + { value: "inactive", label: "Inactive" }, + { value: "none", label: "No connection" }, +]; + +// noAuth providers (e.g. free proxies) are always usable even though they +// never have a stored connection record, so they never fall into "none". +export function getConnectionStatus(stats, isNoAuth = false) { + if (isNoAuth) return "active"; + if (!stats || stats.total === 0) return "none"; + return stats.allDisabled ? "inactive" : "active"; +} + +export function matchesStatusFilter(statusFilter, stats, isNoAuth = false) { + if (statusFilter === "all") return true; + return getConnectionStatus(stats, isNoAuth) === statusFilter; +} diff --git a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/utils.js b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/utils.js index 9fd7d31f..baf34221 100644 --- a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/utils.js +++ b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/utils.js @@ -559,6 +559,21 @@ export function parseQuotaData(provider, data) { } break; + case "groq": + // Requests/Tokens rate-limit windows from response headers — absolute + // used/total (calculatePercentage derives the bar), like Codex/Kiro. + if (data.quotas) { + Object.entries(data.quotas).forEach(([name, quota]) => { + normalizedQuotas.push({ + name, + used: quota.used || 0, + total: quota.total || 0, + resetAt: quota.resetAt || null, + }); + }); + } + break; + case "ollama": // Session (5h) / Weekly (7d) usage % from ollama.com/api/usage. // remainingPercentage only — no absolute remaining (UI treats remaining as %). diff --git a/src/app/api/models/custom/route.js b/src/app/api/models/custom/route.js index 76e14c96..ce917d76 100644 --- a/src/app/api/models/custom/route.js +++ b/src/app/api/models/custom/route.js @@ -1,8 +1,19 @@ import { NextResponse } from "next/server"; import { getCustomModels, addCustomModel, deleteCustomModel } from "@/models"; +import { CAPACITY_META } from "@/shared/constants/models"; export const dynamic = "force-dynamic"; +// Whitelist capability keys to boolean values — ignore anything else +function sanitizeCaps(caps) { + if (!caps || typeof caps !== "object") return null; + const clean = {}; + for (const key of Object.keys(CAPACITY_META)) { + if (typeof caps[key] === "boolean") clean[key] = caps[key]; + } + return Object.keys(clean).length ? clean : null; +} + // GET /api/models/custom - List all custom models export async function GET() { try { @@ -17,11 +28,12 @@ export async function GET() { // POST /api/models/custom - Add custom model export async function POST(request) { try { - const { providerAlias, id, type, name } = await request.json(); + const { providerAlias, id, type, name, caps } = await request.json(); if (!providerAlias || !id) { return NextResponse.json({ error: "providerAlias and id required" }, { status: 400 }); } - const added = await addCustomModel({ providerAlias, id, type: type || "llm", name }); + const cleanCaps = sanitizeCaps(caps); + const added = await addCustomModel({ providerAlias, id, type: type || "llm", name, ...(cleanCaps ? { caps: cleanCaps } : {}) }); return NextResponse.json({ success: true, added }); } catch (error) { console.log("Error adding custom model:", error); diff --git a/src/app/api/models/route.js b/src/app/api/models/route.js index b2e4fe0a..ce5619f9 100644 --- a/src/app/api/models/route.js +++ b/src/app/api/models/route.js @@ -1,5 +1,5 @@ import { NextResponse } from "next/server"; -import { getModelAliases, setModelAlias } from "@/models"; +import { getModelAliases, setModelAlias, getCustomModels } from "@/models"; import { getDisabledModels } from "@/lib/disabledModelsDb"; import { AI_MODELS } from "@/shared/constants/config"; import { getProviderAlias } from "@/shared/constants/providers"; @@ -37,6 +37,33 @@ export async function GET() { }; }); + // Custom models ride along; their stored caps override the name heuristic + const seenFull = new Set(models.map((m) => m.fullModel)); + const customModels = (await getCustomModels()).filter((m) => { + if (!m?.id || (m.kind || m.type || "llm") !== "llm") return false; + return !seenFull.has(`${m.providerAlias}/${m.id}`); + }); + for (const m of customModels) { + const fullModel = `${m.providerAlias}/${m.id}`; + const c = getCapabilitiesForModel(m.providerAlias, m.id); + models.push({ + provider: m.providerAlias, + model: m.id, + name: m.name || m.id, + fullModel, + routedModel: fullModel, + alias: modelAliases[fullModel] || m.id, + caps: { + vision: c.vision, + search: c.search, + reasoning: c.reasoning, + contextWindow: c.contextWindow, + maxOutput: c.maxOutput, + ...(m.caps || {}), + }, + }); + } + return NextResponse.json({ models }); } catch (error) { console.log("Error fetching models:", error); diff --git a/src/app/api/providers/suggested-models/filters.js b/src/app/api/providers/suggested-models/filters.js index 8299f46b..13454757 100644 --- a/src/app/api/providers/suggested-models/filters.js +++ b/src/app/api/providers/suggested-models/filters.js @@ -1,6 +1,9 @@ // Free OpenCode models that don't use the "-free" id suffix const KNOWN_FREE_OPENCODE_MODELS = ["big-pickle"]; +// Upstream returns "Model is unavailable" for this id (2026-09-02) — re-enable when fixed +const DEAD_FREE_OPENCODE_MODELS = new Set(["deepseek-v4-flash-free"]); + export const FILTERS = { "openrouter-free": (models) => models @@ -15,7 +18,7 @@ export const FILTERS = { "opencode-free": (models) => models - .filter((m) => m.id?.endsWith("-free") || KNOWN_FREE_OPENCODE_MODELS.includes(m.id)) + .filter((m) => (m.id?.endsWith("-free") || KNOWN_FREE_OPENCODE_MODELS.includes(m.id)) && !DEAD_FREE_OPENCODE_MODELS.has(m.id)) .map((m) => ({ id: m.id, name: m.id })), // models.dev returns a large catalog; keep only mimo models diff --git a/src/app/api/v1/models/[...model]/route.js b/src/app/api/v1/models/[...model]/route.js new file mode 100644 index 00000000..826d08e6 --- /dev/null +++ b/src/app/api/v1/models/[...model]/route.js @@ -0,0 +1,78 @@ +import { buildModelsList } from "../route.js"; + +// URL slug → service kind(s). `web` covers both webSearch and webFetch. +const KIND_SLUG_MAP = { + "image": ["image"], + "tts": ["tts"], + "stt": ["stt"], + "embedding": ["embedding"], + "image-to-text": ["imageToText"], + "web": ["webSearch", "webFetch"], +}; + +const LLM_KIND = "llm"; + +export async function OPTIONS() { + return new Response(null, { + headers: { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "GET, OPTIONS", + "Access-Control-Allow-Headers": "*", + }, + }); +} + +function json(data, options = {}) { + return Response.json(data, { + ...options, + headers: { + "Access-Control-Allow-Origin": "*", + ...options.headers, + }, + }); +} + +/** + * GET /v1/models/{kind} - OpenAI-compatible models list filtered by capability. + * GET /v1/models/{provider}/{model} - OpenAI-compatible single model lookup. + * Supported kinds: image, tts, stt, embedding, image-to-text, web. + */ +export async function GET(_request, { params }) { + try { + const { model } = await params; + const path = Array.isArray(model) ? model : [model]; + const identifier = path.filter(Boolean).join("/"); + const kindFilter = path.length === 1 ? KIND_SLUG_MAP[identifier] : null; + + if (kindFilter) { + const data = await buildModelsList(kindFilter); + return json({ object: "list", data }); + } + + // Match the same LLM catalog exposed by GET /v1/models. A catch-all + // parameter is required because provider-prefixed IDs contain a slash. + const models = await buildModelsList([LLM_KIND]); + const matchedModel = models.find((candidate) => candidate.id === identifier); + + if (!matchedModel) { + return json( + { + error: { + message: `The model '${identifier}' does not exist or you do not have access to it.`, + type: "invalid_request_error", + code: "model_not_found", + }, + }, + { status: 404 }, + ); + } + + return json(matchedModel); + } catch (error) { + console.log("Error fetching model:", error); + return json( + { error: { message: error.message, type: "server_error" } }, + { status: 500 }, + ); + } +} diff --git a/src/app/api/v1/models/[kind]/route.js b/src/app/api/v1/models/[kind]/route.js deleted file mode 100644 index b0f715ba..00000000 --- a/src/app/api/v1/models/[kind]/route.js +++ /dev/null @@ -1,55 +0,0 @@ -import { buildModelsList } from "../route.js"; - -// URL slug → service kind(s). `web` covers both webSearch and webFetch. -const KIND_SLUG_MAP = { - "image": ["image"], - "tts": ["tts"], - "stt": ["stt"], - "embedding": ["embedding"], - "image-to-text": ["imageToText"], - "web": ["webSearch", "webFetch"], -}; - -export async function OPTIONS() { - return new Response(null, { - headers: { - "Access-Control-Allow-Origin": "*", - "Access-Control-Allow-Methods": "GET, OPTIONS", - "Access-Control-Allow-Headers": "*", - }, - }); -} - -/** - * GET /v1/models/{kind} - OpenAI-compatible models list filtered by capability. - * Supported kinds: image, tts, stt, embedding, image-to-text, web. - */ -export async function GET(_request, { params }) { - try { - const { kind } = await params; - const kindFilter = KIND_SLUG_MAP[kind]; - - if (!kindFilter) { - return Response.json( - { - error: { - message: `Unknown model kind: ${kind}. Supported: ${Object.keys(KIND_SLUG_MAP).join(", ")}`, - type: "invalid_request_error", - }, - }, - { status: 404, headers: { "Access-Control-Allow-Origin": "*" } } - ); - } - - const data = await buildModelsList(kindFilter); - return Response.json({ object: "list", data }, { - headers: { "Access-Control-Allow-Origin": "*" }, - }); - } catch (error) { - console.log("Error fetching models by kind:", error); - return Response.json( - { error: { message: error.message, type: "server_error" } }, - { status: 500 } - ); - } -} diff --git a/src/app/layout.js b/src/app/layout.js index 8e9b7609..a6534188 100644 --- a/src/app/layout.js +++ b/src/app/layout.js @@ -32,6 +32,14 @@ export default function RootLayout({ children }) { return ( + {/* Apply persisted theme before first paint so a reload does not flash the + default (light) theme before the client store hydrates. Mirrors the + zustand-persist "theme" key and the `dark` class applyTheme() sets. */} +