Merge remote-tracking branch 'upstream/master'
This commit is contained in:
commit
c378f2dc90
97 files changed
+4245
-572
No files matched your search
+4
-1
@@ -85,4 +85,7 @@ graphify-out/*
|
|||||||
.script/
|
.script/
|
||||||
.codegraph/
|
.codegraph/
|
||||||
.PR/
|
.PR/
|
||||||
.next-analyze/*
|
.next-analyze/*
|
||||||
|
|
||||||
|
# Kiro local workspace state
|
||||||
|
.kiro/
|
||||||
+31
-1
@@ -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)
|
# v0.5.59 (2026-08-29)
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
@@ -40,6 +67,9 @@
|
|||||||
- **i18n**: pt-BR expanded to 1132 terms
|
- **i18n**: pt-BR expanded to 1132 terms
|
||||||
|
|
||||||
## Fixes
|
## 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
|
- **Stream**: record usage when a client closes on the terminal event — the
|
||||||
Responses API has no [DONE] sentinel, so codex closed the socket on
|
Responses API has no [DONE] sentinel, so codex closed the socket on
|
||||||
`response.completed` and cancelled the reader before flush() ran its usage
|
`response.completed` and cancelled the reader before flush() ran its usage
|
||||||
@@ -738,4 +768,4 @@
|
|||||||
# v0.4.46 (2026-05-15)
|
# v0.4.46 (2026-05-15)
|
||||||
|
|
||||||
## Breaking Changes
|
## Breaking Changes
|
||||||
- Tunnel public URL changed — old tunnel links no longer work, please reconnect to get the new URL
|
- Tunnel public URL changed — old tunnel links no longer work, please reconnect to get the new URL
|
||||||
@@ -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,
|
# 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.
|
# 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
|
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 && \
|
RUN mkdir -p /app/data && chown -R node:node /app && \
|
||||||
mkdir -p /app/data-home && chown node:node /app/data-home && \
|
mkdir -p /app/data-home && chown node:node /app/data-home && \
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "9router",
|
"name": "9router",
|
||||||
"version": "0.5.59",
|
"version": "0.5.65",
|
||||||
"description": "9Router CLI - Start and manage 9Router server",
|
"description": "9Router CLI - Start and manage 9Router server",
|
||||||
"bin": {
|
"bin": {
|
||||||
"9router": "./cli.js"
|
"9router": "./cli.js"
|
||||||
|
|||||||
@@ -53,6 +53,9 @@ const PROVIDER_MODELS = {
|
|||||||
{ id: "glm-4.7" },
|
{ id: "glm-4.7" },
|
||||||
],
|
],
|
||||||
ag: [
|
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-high" },
|
||||||
{ id: "gemini-3.7-flash-medium" },
|
{ id: "gemini-3.7-flash-medium" },
|
||||||
{ id: "gemini-3.7-flash-low" },
|
{ id: "gemini-3.7-flash-low" },
|
||||||
@@ -101,6 +104,8 @@ const PROVIDER_MODELS = {
|
|||||||
{ id: "claude-3-5-sonnet-20241022" },
|
{ id: "claude-3-5-sonnet-20241022" },
|
||||||
],
|
],
|
||||||
gemini: [
|
gemini: [
|
||||||
|
{ id: "gemini-3.8-flash" },
|
||||||
|
{ id: "gemini-3.7-flash" },
|
||||||
{ id: "gemini-3.6-flash" },
|
{ id: "gemini-3.6-flash" },
|
||||||
{ id: "gemini-3.5-flash-lite" },
|
{ id: "gemini-3.5-flash-lite" },
|
||||||
{ id: "gemini-3-pro-preview" },
|
{ id: "gemini-3-pro-preview" },
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ import REGISTRY from "../providers/registry/index.js";
|
|||||||
// PROVIDER_MODELS now built from providers/registry (transport + models co-located)
|
// PROVIDER_MODELS now built from providers/registry (transport + models co-located)
|
||||||
import { PROVIDER_MODELS } from "../providers/index.js";
|
import { PROVIDER_MODELS } from "../providers/index.js";
|
||||||
import { modelQuotaFamily, modelStrip, modelTargetFormat, modelSupportedFormats, normalizeModelId } from "../providers/models/schema.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 };
|
export { PROVIDER_MODELS };
|
||||||
|
|
||||||
|
|
||||||
@@ -49,6 +50,9 @@ export function findModelName(aliasOrId, modelId) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getModelTargetFormat(aliasOrId, modelId) {
|
export function getModelTargetFormat(aliasOrId, modelId) {
|
||||||
|
if ((!aliasOrId || aliasOrId === "oc" || aliasOrId === "opencode") && isMuseSparkModel(modelId)) {
|
||||||
|
return FORMATS.OPENAI_RESPONSES;
|
||||||
|
}
|
||||||
const models = PROVIDER_MODELS[aliasOrId];
|
const models = PROVIDER_MODELS[aliasOrId];
|
||||||
if (!models) return null;
|
if (!models) return null;
|
||||||
return modelTargetFormat(findModel(models, modelId, aliasOrId));
|
return modelTargetFormat(findModel(models, modelId, aliasOrId));
|
||||||
|
|||||||
@@ -4,10 +4,14 @@ import { PROVIDERS } from "../config/providers.js";
|
|||||||
import { getThinkingLevels } from "../providers/thinkingLevels.js";
|
import { getThinkingLevels } from "../providers/thinkingLevels.js";
|
||||||
import { injectReasoningContent } from "../utils/reasoningContentInjector.js";
|
import { injectReasoningContent } from "../utils/reasoningContentInjector.js";
|
||||||
import { resolveSessionId } from "../utils/sessionManager.js";
|
import { resolveSessionId } from "../utils/sessionManager.js";
|
||||||
|
import { isMuseSparkModel } from "../providers/models/helpers.js";
|
||||||
|
|
||||||
const OPENCODE_UA = "opencode";
|
const OPENCODE_UA = "opencode";
|
||||||
// Models served by /zen/v1/responses; every other model stays on /chat/completions.
|
// 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() {
|
function generateRequestId() {
|
||||||
return `msg_${crypto.randomUUID().replace(/-/g, "")}`;
|
return `msg_${crypto.randomUUID().replace(/-/g, "")}`;
|
||||||
@@ -23,7 +27,8 @@ function baseModelId(model) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function isResponsesModel(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) {
|
function resolveOpencodeSession(body, credentials) {
|
||||||
|
|||||||
@@ -25,10 +25,16 @@ export function extractUsageFromResponse(responseBody) {
|
|||||||
if (!responseBody || typeof responseBody !== "object") return null;
|
if (!responseBody || typeof responseBody !== "object") return null;
|
||||||
|
|
||||||
// Claude format
|
// 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) {
|
if (responseBody.usage?.input_tokens !== undefined) {
|
||||||
return {
|
return {
|
||||||
prompt_tokens: responseBody.usage.input_tokens || 0,
|
prompt_tokens: responseBody.usage.input_tokens || 0,
|
||||||
completion_tokens: responseBody.usage.output_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_read_input_tokens: responseBody.usage.cache_read_input_tokens,
|
||||||
cache_creation_input_tokens: responseBody.usage.cache_creation_input_tokens
|
cache_creation_input_tokens: responseBody.usage.cache_creation_input_tokens
|
||||||
};
|
};
|
||||||
@@ -39,7 +45,7 @@ export function extractUsageFromResponse(responseBody) {
|
|||||||
return {
|
return {
|
||||||
prompt_tokens: responseBody.usage.prompt_tokens || 0,
|
prompt_tokens: responseBody.usage.prompt_tokens || 0,
|
||||||
completion_tokens: responseBody.usage.completion_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
|
reasoning_tokens: responseBody.usage.completion_tokens_details?.reasoning_tokens
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
// Returns normalized shape across all providers
|
||||||
|
|
||||||
const DEFAULT_TIMEOUT_MS = 15000;
|
const DEFAULT_TIMEOUT_MS = 15000;
|
||||||
@@ -56,8 +56,8 @@ function parseJinaTitle(text) {
|
|||||||
return m ? m[1].trim() : null;
|
return m ? m[1].trim() : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildData({ provider, url, title, format, text, costUsd, responseMs, upstreamMs }) {
|
function buildData({ provider, url, title, format, text, links, costUsd, responseMs, upstreamMs }) {
|
||||||
return {
|
const data = {
|
||||||
provider,
|
provider,
|
||||||
url,
|
url,
|
||||||
title: title || null,
|
title: title || null,
|
||||||
@@ -66,6 +66,8 @@ function buildData({ provider, url, title, format, text, costUsd, responseMs, up
|
|||||||
usage: { fetch_cost_usd: costUsd ?? null },
|
usage: { fetch_cost_usd: costUsd ?? null },
|
||||||
metrics: { response_time_ms: responseMs, upstream_latency_ms: upstreamMs }
|
metrics: { response_time_ms: responseMs, upstream_latency_ms: upstreamMs }
|
||||||
};
|
};
|
||||||
|
if (Array.isArray(links)) data.links = links;
|
||||||
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function readJsonOrText(res) {
|
async function readJsonOrText(res) {
|
||||||
@@ -115,6 +117,18 @@ export async function handleFetchCore({ url, format, maxCharacters, provider, pr
|
|||||||
if (provider === "exa") {
|
if (provider === "exa") {
|
||||||
return await runExa({ url, fmt, timeoutMs, apiKey, maxCharacters, costPerQuery, startedAt });
|
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}` };
|
return { success: false, status: 400, error: `Unsupported provider: ${provider}` };
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log?.("fetch handler error:", err?.message || 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
|
||||||
|
})
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@
|
|||||||
import { buildSearchRequest } from "./callers.js";
|
import { buildSearchRequest } from "./callers.js";
|
||||||
import { normalizeSearchResponse } from "./normalizers.js";
|
import { normalizeSearchResponse } from "./normalizers.js";
|
||||||
import { handleChatSearch } from "./chatSearch.js";
|
import { handleChatSearch } from "./chatSearch.js";
|
||||||
|
import { fetchPublic } from "../../../src/shared/utils/ssrfGuard.js";
|
||||||
|
|
||||||
const GLOBAL_TIMEOUT_MS = 15000;
|
const GLOBAL_TIMEOUT_MS = 15000;
|
||||||
const NON_RETRIABLE = new Set([400, 401, 403, 404]);
|
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}`);
|
log?.info?.("SEARCH", `${provider.id} | "${params.query.slice(0, 80)}" | type=${params.searchType}`);
|
||||||
|
|
||||||
try {
|
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);
|
clearTimeout(timer);
|
||||||
if (!resp.ok) {
|
if (!resp.ok) {
|
||||||
const errText = await resp.text().catch(() => "");
|
const errText = await resp.text().catch(() => "");
|
||||||
|
|||||||
@@ -83,7 +83,8 @@ export function capabilitiesFromServiceKind(kind) {
|
|||||||
* otherwise mis-match. Only declare deltas vs DEFAULT.
|
* otherwise mis-match. Only declare deltas vs DEFAULT.
|
||||||
*/
|
*/
|
||||||
export const MODEL_CAPABILITIES = {
|
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": { 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-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 },
|
"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-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": { 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 },
|
"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.
|
// OpenCode Free Muse Spark — multimodal (text+image per models.dev meta/muse-spark)
|
||||||
"muse-spark-1.2-contributor-free": { reasoning: true, thinkingFormat: "openai", contextWindow: 1048576, maxOutput: 131072 },
|
// 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 };
|
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.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 },
|
"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-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-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-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 },
|
"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) ─
|
// ── Gemini (all 2.0+ multimodal + google_search grounding, 1M ctx) ─
|
||||||
{ pattern: "*gemini*image*", caps: { vision: true, imageOutput: true, contextWindow: 1048576 } },
|
{ 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.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*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 } },
|
{ 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-s-2.1*", caps: { reasoning: true, thinkingFormat: "openai", contextWindow: 1000000, maxOutput: 32000 } },
|
||||||
{ pattern: "*laguna*", caps: { reasoning: true, thinkingFormat: "openai", contextWindow: 200000, 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 ───────────────────────────────────────────────────────
|
// ── Others ───────────────────────────────────────────────────────
|
||||||
{ pattern: "*hunyuan*", caps: { reasoning: true, thinkingFormat: "hunyuan", contextWindow: 262144, maxOutput: 262144 } },
|
{ pattern: "*hunyuan*", caps: { reasoning: true, thinkingFormat: "hunyuan", contextWindow: 262144, maxOutput: 262144 } },
|
||||||
{ pattern: "hy3*", caps: { reasoning: true, thinkingFormat: "hunyuan", contextWindow: 262144, maxOutput: 262144 } },
|
{ pattern: "hy3*", caps: { reasoning: true, thinkingFormat: "hunyuan", contextWindow: 262144, maxOutput: 262144 } },
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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 },
|
"o1-mini": { input: 3.00, output: 12.00, cached: 1.50, reasoning: 18.00, cache_creation: 3.00 },
|
||||||
|
|
||||||
// === Gemini ===
|
// === 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": { 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-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 },
|
"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-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.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.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 },
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -36,8 +36,7 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
usage: {
|
usage: {
|
||||||
// Discovery (quota/project) on PROD; daily host rejects these.
|
quotaApiUrl: `${ANTIGRAVITY_IDE_BASE_URL}/v1internal:fetchAvailableModels`,
|
||||||
quotaApiUrl: "https://cloudcode-pa.googleapis.com/v1internal:fetchAvailableModels",
|
|
||||||
loadProjectApiUrl: "https://cloudcode-pa.googleapis.com/v1internal:loadCodeAssist",
|
loadProjectApiUrl: "https://cloudcode-pa.googleapis.com/v1internal:loadCodeAssist",
|
||||||
tokenUrl: "https://oauth2.googleapis.com/token",
|
tokenUrl: "https://oauth2.googleapis.com/token",
|
||||||
},
|
},
|
||||||
@@ -45,6 +44,10 @@ export default {
|
|||||||
clientSecret: "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",
|
clientSecret: "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",
|
||||||
},
|
},
|
||||||
models: [
|
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-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-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)" },
|
{ id: "gemini-3.7-flash-low", name: "Gemini 3.7 Flash (Low)", upstreamModelId: "gemini-3.7-flash-tiered(low)" },
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { CLAUDE_CLI_SPOOF_HEADERS } from "../shared.js";
|
import { CLAUDE_CLI_VERSION } from "../shared.js";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
id: "claude",
|
id: "claude",
|
||||||
@@ -25,7 +25,7 @@ export default {
|
|||||||
"Anthropic-Version": "2023-06-01",
|
"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-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",
|
"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-App": "cli",
|
||||||
"X-Stainless-Helper-Method": "stream",
|
"X-Stainless-Helper-Method": "stream",
|
||||||
"X-Stainless-Retry-Count": "0",
|
"X-Stainless-Retry-Count": "0",
|
||||||
@@ -58,6 +58,7 @@ export default {
|
|||||||
},
|
},
|
||||||
models: [
|
models: [
|
||||||
{ id: "claude-opus-5", name: "Claude Opus 5" },
|
{ 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-fable-5", name: "Claude Fable 5" },
|
||||||
{ id: "claude-sonnet-5", name: "Claude Sonnet 5" },
|
{ id: "claude-sonnet-5", name: "Claude Sonnet 5" },
|
||||||
{ id: "claude-haiku-4-5-20251001", name: "Claude 4.5 Haiku" },
|
{ id: "claude-haiku-4-5-20251001", name: "Claude 4.5 Haiku" },
|
||||||
|
|||||||
@@ -47,16 +47,24 @@ export default {
|
|||||||
models: [
|
models: [
|
||||||
{ id: "glm-5.2", name: "GLM-5.2" },
|
{ id: "glm-5.2", name: "GLM-5.2" },
|
||||||
{ id: "glm-5.1", name: "GLM-5.1" },
|
{ 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-5.0-turbo", name: "GLM-5.0-Turbo" },
|
||||||
{ id: "glm-5v-turbo", name: "GLM-5v-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-m3", name: "MiniMax-M3" },
|
||||||
{ id: "minimax-m2.7", name: "MiniMax-M2.7" },
|
{ id: "minimax-m2.7", name: "MiniMax-M2.7" },
|
||||||
{ id: "kimi-k2.7", name: "Kimi-K2.7-Code" },
|
{ id: "kimi-k2.7", name: "Kimi-K2.7-Code" },
|
||||||
{ id: "kimi-k2.6", name: "Kimi-K2.6" },
|
{ id: "kimi-k2.6", name: "Kimi-K2.6" },
|
||||||
{ id: "kimi-k2.5", name: "Kimi-K2.5" },
|
{ 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-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-pro", name: "DeepSeek-V4-Pro" },
|
||||||
{ id: "deepseek-v4-flash", name: "DeepSeek-V4-Flash" },
|
{ id: "deepseek-v4-flash", name: "DeepSeek-V4-Flash" },
|
||||||
{ id: "deepseek-v3-2-volc", name: "DeepSeek-V3.2" },
|
{ id: "deepseek-v3-2-volc", name: "DeepSeek-V3.2" },
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
models: [
|
models: [
|
||||||
|
{ id: "gemini-3.8-flash", name: "Gemini 3.8 Flash" },
|
||||||
{ id: "gemini-3.7-flash", name: "Gemini 3.7 Flash" },
|
{ id: "gemini-3.7-flash", name: "Gemini 3.7 Flash" },
|
||||||
{ id: "gemini-3.6-flash", name: "Gemini 3.6 Flash" },
|
{ id: "gemini-3.6-flash", name: "Gemini 3.6 Flash" },
|
||||||
{ id: "gemini-3.5-flash-lite", name: "Gemini 3.5 Flash Lite" },
|
{ id: "gemini-3.5-flash-lite", name: "Gemini 3.5 Flash Lite" },
|
||||||
|
|||||||
@@ -17,6 +17,12 @@ export default {
|
|||||||
transport: {
|
transport: {
|
||||||
baseUrl: "https://api.groq.com/openai/v1/chat/completions",
|
baseUrl: "https://api.groq.com/openai/v1/chat/completions",
|
||||||
validateUrl: "https://api.groq.com/openai/v1/models",
|
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: [
|
models: [
|
||||||
{ id: "llama-3.3-70b-versatile", name: "Llama 3.3 70B" },
|
{ id: "llama-3.3-70b-versatile", name: "Llama 3.3 70B" },
|
||||||
@@ -34,4 +40,8 @@ export default {
|
|||||||
authHeader: "bearer",
|
authHeader: "bearer",
|
||||||
format: "openai",
|
format: "openai",
|
||||||
},
|
},
|
||||||
|
features: {
|
||||||
|
usage: true,
|
||||||
|
usageApikey: true,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
@@ -31,7 +31,16 @@ export default {
|
|||||||
{ id: "qwen3.5", name: "Qwen3.5" },
|
{ id: "qwen3.5", name: "Qwen3.5" },
|
||||||
{ id: "minimax-m3", name: "MiniMax M3" },
|
{ 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: {
|
features: {
|
||||||
usage: true,
|
usage: true,
|
||||||
usageApikey: true,
|
usageApikey: true,
|
||||||
|
|||||||
@@ -20,9 +20,10 @@ export default {
|
|||||||
noAuth: true,
|
noAuth: true,
|
||||||
},
|
},
|
||||||
models: [
|
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.
|
// /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.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" },
|
modelsFetcher: { url: "https://opencode.ai/zen/v1/models", type: "opencode-free" },
|
||||||
passthroughModels: true,
|
passthroughModels: true,
|
||||||
|
|||||||
@@ -24,129 +24,31 @@ export default {
|
|||||||
validateUrl: "https://api.tokenrouter.com/v1/models",
|
validateUrl: "https://api.tokenrouter.com/v1/models",
|
||||||
thinkingFormat: "tokenrouter",
|
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.
|
// fetched via modelsFetcher; other ids still accepted via passthroughModels.
|
||||||
models: [
|
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-haiku-4.5", name: "Claude Haiku 4.5" },
|
||||||
{ id: "anthropic/claude-opus-4.5", name: "Claude Opus 4.5" },
|
{ id: "anthropic/claude-sonnet-4.6", name: "Claude Sonnet 4.6" },
|
||||||
{ 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-opus-4.8", name: "Claude Opus 4.8" },
|
{ 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-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", 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-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.4-pro", name: "Gpt 5.4 Pro" },
|
||||||
{ id: "openai/gpt-5.5", name: "Gpt 5.5" },
|
{ 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-sol", name: "Gpt 5.6 Sol" },
|
||||||
{ id: "openai/gpt-5.6-terra", name: "Gpt 5.6 Terra" },
|
{ id: "google/gemini-3.5-flash", name: "Gemini 3.5 Flash" },
|
||||||
{ id: "openai/gpt-audio", name: "Gpt Audio", kind: "audio" },
|
{ id: "google/gemini-3.6-flash", name: "Gemini 3.6 Flash" },
|
||||||
{ id: "openai/gpt-audio-mini", name: "Gpt Audio Mini", kind: "audio" },
|
{ id: "deepseek/deepseek-v4-flash", name: "Deepseek V4 Flash" },
|
||||||
{ id: "openai/gpt-oss-120b", name: "Gpt Oss 120B" },
|
{ id: "deepseek/deepseek-v4-pro", name: "Deepseek V4 Pro" },
|
||||||
{ id: "qwen/qwen3-coder-next", name: "Qwen3 Coder Next" },
|
{ 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-max", name: "Qwen3.7 Max" },
|
||||||
{ id: "qwen/qwen3.7-plus", name: "Qwen3.7 Plus" },
|
|
||||||
{ id: "qwen/qwen3.8-max", name: "Qwen3.8 Max" },
|
{ id: "qwen/qwen3.8-max", name: "Qwen3.8 Max" },
|
||||||
{ id: "qwen3.5-omni-plus", name: "Qwen3.5 Omni Plus" },
|
{ id: "moonshotai/kimi-k2.7-code", name: "Kimi K2.7 Code" },
|
||||||
{ id: "qwen3.6-flash", name: "Qwen3.6 Flash" },
|
{ id: "moonshotai/kimi-k3-free", name: "Kimi K3 Free" },
|
||||||
{ id: "sakana/fugu-ultra", name: "Fugu Ultra" },
|
{ id: "z-ai/glm-5.3-free", name: "Glm 5.3 Free" },
|
||||||
{ 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: "z-ai/glm-5.2", name: "Glm 5.2" },
|
{ 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"],
|
serviceKinds: ["llm", "embedding", "image"],
|
||||||
embeddingConfig: {
|
embeddingConfig: {
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export function mapStainlessArch() {
|
|||||||
|
|
||||||
// Anthropic API version (single source — reused across claude-format providers/executors)
|
// Anthropic API version (single source — reused across claude-format providers/executors)
|
||||||
export const ANTHROPIC_API_VERSION = "2023-06-01";
|
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)
|
// Shared Claude-compatible API headers (reused across claude-format providers)
|
||||||
export const CLAUDE_API_HEADERS = {
|
export const CLAUDE_API_HEADERS = {
|
||||||
@@ -34,7 +35,7 @@ export const CLAUDE_CLI_SPOOF_HEADERS = {
|
|||||||
"Anthropic-Version": ANTHROPIC_API_VERSION,
|
"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-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",
|
"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-App": "cli",
|
||||||
"X-Stainless-Helper-Method": "stream",
|
"X-Stainless-Helper-Method": "stream",
|
||||||
"X-Stainless-Retry-Count": "0",
|
"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 OPENAI_COMPAT_BASE = "https://api.openai.com/v1";
|
||||||
export const ANTHROPIC_COMPAT_BASE = "https://api.anthropic.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
|
// Keep this static even when 9router runs on Linux: the provider profile is
|
||||||
// intentionally matching the IDE client, not the server host.
|
// 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_BASE_URL = "https://daily-cloudcode-pa.googleapis.com";
|
||||||
export const ANTIGRAVITY_IDE_USER_AGENT = `antigravity/ide/${ANTIGRAVITY_IDE_VERSION} darwin/arm64`;
|
export const ANTIGRAVITY_IDE_USER_AGENT = `antigravity/ide/${ANTIGRAVITY_IDE_VERSION} darwin/arm64`;
|
||||||
|
|
||||||
|
|||||||
@@ -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-terra*", levels: [...CODEX_GPT_5_6_LEVELS, "ultra"] },
|
||||||
{ provider: "codex", pattern: "*gpt-5.6-luna*", levels: CODEX_GPT_5_6_LEVELS },
|
{ provider: "codex", pattern: "*gpt-5.6-luna*", levels: CODEX_GPT_5_6_LEVELS },
|
||||||
{ pattern: "*codex*", levels: ["low", "medium", "high", "xhigh"] }, // codex cannot disable thinking
|
{ 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.
|
// Returns valid thinking levels for a model, or null when the model has no reasoning.
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { getGrokCliUsage } from "./usage/grok-cli.js";
|
|||||||
import { getKimiUsage } from "./usage/kimi.js";
|
import { getKimiUsage } from "./usage/kimi.js";
|
||||||
import { getDeepseekUsage } from "./usage/deepseek.js";
|
import { getDeepseekUsage } from "./usage/deepseek.js";
|
||||||
import { getFreebuffUsage } from "./usage/freebuff.js";
|
import { getFreebuffUsage } from "./usage/freebuff.js";
|
||||||
|
import { getGroqUsage } from "./usage/groq.js";
|
||||||
import { getZedUsage } from "./usage/zed.js";
|
import { getZedUsage } from "./usage/zed.js";
|
||||||
import { resolveQoderCredentials } from "./qoderModels.js";
|
import { resolveQoderCredentials } from "./qoderModels.js";
|
||||||
import { getGlmUsage } from "./usage/glm.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),
|
kimi: (c) => getKimiUsage(c.accessToken, c.apiKey, c.proxyOptions, c.providerSpecificData),
|
||||||
deepseek: (c) => getDeepseekUsage(c.apiKey, c.proxyOptions),
|
deepseek: (c) => getDeepseekUsage(c.apiKey, c.proxyOptions),
|
||||||
freebuff: (c) => getFreebuffUsage(c.accessToken, c.providerSpecificData, 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),
|
zed: (c) => getZedUsage(c.accessToken, c.providerSpecificData, c.proxyOptions),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -161,6 +161,9 @@ export async function getAntigravityUsage(accessToken, providerSpecificData, pro
|
|||||||
if (data.models) {
|
if (data.models) {
|
||||||
// Filter only recommended/important models (must match PROVIDER_MODELS ag ids)
|
// Filter only recommended/important models (must match PROVIDER_MODELS ag ids)
|
||||||
const importantModels = [
|
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-high',
|
||||||
'gemini-3.7-flash-medium',
|
'gemini-3.7-flash-medium',
|
||||||
'gemini-3.7-flash-low',
|
'gemini-3.7-flash-low',
|
||||||
|
|||||||
@@ -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}` };
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -105,12 +105,16 @@ export function extractThinking(body) {
|
|||||||
// at the call-site where intent is snapshotted before format translation.
|
// at the call-site where intent is snapshotted before format translation.
|
||||||
export const captureThinking = extractThinking;
|
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) {
|
function resolveFormat(targetFormat, model, provider) {
|
||||||
const providerFmt = provider ? PROVIDERS[provider]?.thinkingFormat : null;
|
const providerFmt = provider ? PROVIDERS[provider]?.thinkingFormat : null;
|
||||||
if (providerFmt) return providerFmt;
|
if (providerFmt) return providerFmt;
|
||||||
const caps = getCapabilitiesForModel(provider, model);
|
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";
|
return FORMAT_TO_NATIVE[targetFormat] || "openai";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -237,12 +241,10 @@ function applyFormat(fmt, body, cfg, caps, supportedLevels) {
|
|||||||
}
|
}
|
||||||
case "claude-adaptive": {
|
case "claude-adaptive": {
|
||||||
if (none && canDisable) { body.thinking = { type: "disabled" }; break; }
|
if (none && canDisable) { body.thinking = { type: "disabled" }; break; }
|
||||||
// output_config.effort alone does NOT turn thinking on: Anthropic requires
|
// Models that can disable thinking need the explicit adaptive switch.
|
||||||
// an explicit thinking:{type:"adaptive"} on Opus 4.6/4.7/4.8 and Sonnet 4.6
|
// Permanently adaptive models such as Fable 5.1 accept effort directly.
|
||||||
// ("thinking is off unless you explicitly set it"), and Anthropic-compatible
|
if (canDisable) body.thinking = { type: "adaptive" };
|
||||||
// shims (e.g. GitHub Copilot /v1/messages) default thinking off even for
|
else delete body.thinking;
|
||||||
// Sonnet 5. Send both fields — the documented adaptive-thinking shape.
|
|
||||||
body.thinking = { type: "adaptive" };
|
|
||||||
const level = toLevel(eff);
|
const level = toLevel(eff);
|
||||||
body.output_config = { effort: level === "xhigh" ? "high" : level };
|
body.output_config = { effort: level === "xhigh" ? "high" : level };
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -12,6 +12,18 @@ import { DEFAULT_MAX_TOKENS } from "../../config/runtimeConfig.js";
|
|||||||
const CACHE_CONTROL_5M = { type: "ephemeral" };
|
const CACHE_CONTROL_5M = { type: "ephemeral" };
|
||||||
const CACHE_CONTROL_1H = { type: "ephemeral", ttl: "1h" };
|
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
|
// Check if message has valid non-empty content
|
||||||
export function hasValidContent(msg) {
|
export function hasValidContent(msg) {
|
||||||
if (typeof msg.content === "string" && msg.content.trim()) return true;
|
if (typeof msg.content === "string" && msg.content.trim()) return true;
|
||||||
@@ -108,11 +120,24 @@ function buildThinkingPlaceholder(provider) {
|
|||||||
return block;
|
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.
|
// 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:
|
// Newer Cowork/Claude Code clients emit beta-only shapes that OAuth endpoints reject:
|
||||||
// 1. thinking.type "adaptive" → unsupported on Haiku
|
// 1. thinking.type "adaptive" → unsupported on Haiku
|
||||||
// 2. output_config.effort → unsupported on Haiku
|
// 2. output_config.effort → unsupported on Haiku
|
||||||
// 3. role "system" messages (mid-conversation-system beta) → only top-level system is allowed
|
// 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 = "") {
|
export function normalizeClaudePassthrough(body, model = "") {
|
||||||
if (!body || typeof body !== "object") return body;
|
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,
|
// 3. Drop thinking blocks whose signature is not Claude's (combo mixes models,
|
||||||
// so foreign signatures leak into history and Anthropic rejects them).
|
// so foreign signatures leak into history and Anthropic rejects them).
|
||||||
const thinkingEnabled = body.thinking?.type === "enabled";
|
const thinkingEnabled = body.thinking?.type === "enabled";
|
||||||
|
const droppedServerToolUseIds = new Set();
|
||||||
if (Array.isArray(body.messages)) {
|
if (Array.isArray(body.messages)) {
|
||||||
for (const msg of body.messages) {
|
for (const msg of body.messages) {
|
||||||
if (msg.role !== ROLE.ASSISTANT || !Array.isArray(msg.content)) continue;
|
if (msg.role !== ROLE.ASSISTANT || !Array.isArray(msg.content)) continue;
|
||||||
@@ -178,6 +204,10 @@ export function normalizeClaudePassthrough(body, model = "") {
|
|||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (hasForeignServerToolUseId(block)) {
|
||||||
|
if (block.id != null) droppedServerToolUseIds.add(String(block.id));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (block.type === CLAUDE_BLOCK.TOOL_USE) hasToolUse = true;
|
if (block.type === CLAUDE_BLOCK.TOOL_USE) hasToolUse = true;
|
||||||
kept.push(block);
|
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;
|
return body;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -223,7 +282,7 @@ export function anchorClaudeCache(body) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (Array.isArray(body.tools)) {
|
if (Array.isArray(body.tools)) {
|
||||||
const last = body.tools.length - 1;
|
const last = lastCacheableToolIndex(body.tools);
|
||||||
body.tools.forEach((tool, i) => {
|
body.tools.forEach((tool, i) => {
|
||||||
if (i === last) tool.cache_control = { ...CACHE_CONTROL_1H };
|
if (i === last) tool.cache_control = { ...CACHE_CONTROL_1H };
|
||||||
else delete tool.cache_control;
|
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) => {
|
body.tools = body.tools.map((tool, i) => {
|
||||||
const { cache_control, ...rest } = tool;
|
const { cache_control, ...rest } = tool;
|
||||||
if (i === body.tools.length - 1) {
|
if (i === lastCacheable) {
|
||||||
return { ...rest, cache_control: { type: "ephemeral", ttl: "1h" } };
|
return { ...rest, cache_control: { type: "ephemeral", ttl: "1h" } };
|
||||||
}
|
}
|
||||||
return rest;
|
return rest;
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ export const UNSUPPORTED_SCHEMA_CONSTRAINTS = [
|
|||||||
"uniqueItems", "contains",
|
"uniqueItems", "contains",
|
||||||
// 2020-12 keywords with no Gemini equivalent
|
// 2020-12 keywords with no Gemini equivalent
|
||||||
"unevaluatedProperties", "unevaluatedItems", "contentSchema",
|
"unevaluatedProperties", "unevaluatedItems", "contentSchema",
|
||||||
|
// Tuple-array keywords; converted to items first, leftovers stripped
|
||||||
|
"prefixItems", "additionalItems",
|
||||||
// Claude rejects these in VALIDATED mode
|
// Claude rejects these in VALIDATED mode
|
||||||
"default", "examples",
|
"default", "examples",
|
||||||
// JSON Schema meta keywords
|
// 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);
|
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
|
// Clean JSON Schema for Antigravity API compatibility - removes unsupported keywords recursively
|
||||||
export function cleanJSONSchemaForAntigravity(schema) {
|
export function cleanJSONSchemaForAntigravity(schema) {
|
||||||
if (!schema || typeof schema !== "object") return schema;
|
if (!schema || typeof schema !== "object") return schema;
|
||||||
@@ -321,11 +354,13 @@ export function cleanJSONSchemaForAntigravity(schema) {
|
|||||||
|
|
||||||
// Phase 2: Flatten complex structures
|
// Phase 2: Flatten complex structures
|
||||||
mergeAllOf(cleaned);
|
mergeAllOf(cleaned);
|
||||||
|
convertPrefixItems(cleaned);
|
||||||
flattenAnyOfOneOf(cleaned);
|
flattenAnyOfOneOf(cleaned);
|
||||||
flattenTypeArrays(cleaned);
|
flattenTypeArrays(cleaned);
|
||||||
|
|
||||||
// Phase 2.5: Infer missing type=object when properties exist (Gemini requirement)
|
// Phase 2.5: Infer missing type=object when properties exist (Gemini requirement)
|
||||||
ensureObjectType(cleaned);
|
ensureObjectType(cleaned);
|
||||||
|
ensureArrayItems(cleaned);
|
||||||
|
|
||||||
// Phase 3: Remove all unsupported keywords at ALL levels (including inside arrays)
|
// Phase 3: Remove all unsupported keywords at ALL levels (including inside arrays)
|
||||||
removeUnsupportedKeywords(cleaned, UNSUPPORTED_SCHEMA_CONSTRAINTS);
|
removeUnsupportedKeywords(cleaned, UNSUPPORTED_SCHEMA_CONSTRAINTS);
|
||||||
|
|||||||
@@ -327,7 +327,6 @@ export function claudeToKiroRequest(model, body, stream, credentials) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (profileArn) payload.profileArn = profileArn;
|
if (profileArn) payload.profileArn = profileArn;
|
||||||
if (systemPrompt) payload.systemPrompt = systemPrompt;
|
|
||||||
if (additionalModelRequestFields) {
|
if (additionalModelRequestFields) {
|
||||||
payload.additionalModelRequestFields = additionalModelRequestFields;
|
payload.additionalModelRequestFields = additionalModelRequestFields;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -420,7 +420,6 @@ export function openaiToKiroRequest(model, body, stream, credentials) {
|
|||||||
if (profileArn) {
|
if (profileArn) {
|
||||||
payload.profileArn = profileArn;
|
payload.profileArn = profileArn;
|
||||||
}
|
}
|
||||||
if (systemPrompt) payload.systemPrompt = systemPrompt;
|
|
||||||
if (additionalModelRequestFields) {
|
if (additionalModelRequestFields) {
|
||||||
payload.additionalModelRequestFields = additionalModelRequestFields;
|
payload.additionalModelRequestFields = additionalModelRequestFields;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ export const CLAUDE_BLOCK = {
|
|||||||
TOOL_RESULT: "tool_result",
|
TOOL_RESULT: "tool_result",
|
||||||
THINKING: "thinking",
|
THINKING: "thinking",
|
||||||
REDACTED_THINKING: "redacted_thinking",
|
REDACTED_THINKING: "redacted_thinking",
|
||||||
|
SERVER_TOOL_USE: "server_tool_use",
|
||||||
|
WEB_SEARCH_TOOL_RESULT: "web_search_tool_result",
|
||||||
};
|
};
|
||||||
|
|
||||||
// OpenAI Responses API item types.
|
// OpenAI Responses API item types.
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
import { createHash, randomBytes, randomUUID } from "crypto";
|
import { createHash, randomBytes, randomUUID } from "crypto";
|
||||||
import { CLAUDE_TOOL_SUFFIX, CC_DEFAULT_TOOLS } from "../config/appConstants.js";
|
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";
|
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=<ver>.<build>; cc_entrypoint=sdk-cli; cch=<hash>;
|
// x-anthropic-billing-header: cc_version=<ver>.<build>; cc_entrypoint=sdk-cli; cch=<hash>;
|
||||||
function generateBillingHeader(payload) {
|
function generateBillingHeader(payload) {
|
||||||
const content = JSON.stringify(payload);
|
const content = JSON.stringify(payload);
|
||||||
const cch = createHash("sha256").update(content).digest("hex").slice(0, 5);
|
const cch = createHash("sha256").update(content).digest("hex").slice(0, 5);
|
||||||
const buildHash = randomBytes(2).toString("hex").slice(0, 3);
|
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)
|
// 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)}`;
|
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":"<uuid>","session_id":"<uuid>"}
|
// {"device_id":"<64hex>","account_uuid":"<uuid>","session_id":"<uuid>"}
|
||||||
// device_id/account_uuid derive from apiKey (stable per account), session_id per-conversation
|
// device_id/account_uuid derive from apiKey (stable per account), session_id per-conversation
|
||||||
function generateFakeUserID(sessionId, apiKey) {
|
function generateFakeUserID(sessionId, apiKey) {
|
||||||
|
|||||||
@@ -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() };
|
||||||
|
}
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "9router-app",
|
"name": "9router-app",
|
||||||
"version": "0.5.59",
|
"version": "0.5.65",
|
||||||
"description": "9Router web dashboard",
|
"description": "9Router web dashboard",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
+1379
-183
File diff suppressed because it is too large.
Load diff
@@ -235,6 +235,7 @@
|
|||||||
"Codex CLI not installed": "Codex CLI 未安装",
|
"Codex CLI not installed": "Codex CLI 未安装",
|
||||||
"Codex Reset Credit Expiry": "Codex 重置信用有效期",
|
"Codex Reset Credit Expiry": "Codex 重置信用有效期",
|
||||||
"Codex uses": "Codex 使用",
|
"Codex uses": "Codex 使用",
|
||||||
|
"Combo & Vision Adapter": "组合与视觉适配器",
|
||||||
"Combo Name": "组合名称",
|
"Combo Name": "组合名称",
|
||||||
"Combo Round Robin": "组合轮询",
|
"Combo Round Robin": "组合轮询",
|
||||||
"Combo Sticky Limit": "组合粘性限制",
|
"Combo Sticky Limit": "组合粘性限制",
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 3.5 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 3.5 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 5.3 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.2 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.2 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.2 KiB |
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: 9router-web-fetch
|
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
|
# 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}'
|
-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:
|
JS:
|
||||||
|
|
||||||
@@ -83,12 +94,15 @@ console.log(data.title, data.content.length);
|
|||||||
"url": "...",
|
"url": "...",
|
||||||
"title": "...",
|
"title": "...",
|
||||||
"content": { "format": "markdown", "text": "...", "length": 1234 },
|
"content": { "format": "markdown", "text": "...", "length": 1234 },
|
||||||
|
"links": ["https://example.com/related"],
|
||||||
"metadata": { "author": null, "published_at": null, "language": null },
|
"metadata": { "author": null, "published_at": null, "language": null },
|
||||||
"usage": { "fetch_cost_usd": 0 },
|
"usage": { "fetch_cost_usd": 0 },
|
||||||
"metrics": { "response_time_ms": 850, "upstream_latency_ms": 700 }
|
"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 quirks
|
||||||
|
|
||||||
| Provider | Auth | Best for |
|
| 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 |
|
| `jina-reader` | Bearer (optional) | Free tier (~1M chars/mo); fastest plain markdown |
|
||||||
| `tavily` | Bearer | Bulk extract; returns `raw_content` |
|
| `tavily` | Bearer | Bulk extract; returns `raw_content` |
|
||||||
| `exa` | `x-api-key` | Pre-indexed pages; fast text extraction |
|
| `exa` | `x-api-key` | Pre-indexed pages; fast text extraction |
|
||||||
|
| `ollama` | Bearer | Markdown plus page title and discovered links; uses the Ollama Cloud key |
|
||||||
@@ -1,38 +1,76 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
|
import { readKeyPresets, upsertKeyPreset, deleteKeyPreset, subscribeKeyPresets } from "./cliEndpointPresets";
|
||||||
|
|
||||||
const CUSTOM_VALUE = "__custom__";
|
const CUSTOM_VALUE = "__custom__";
|
||||||
|
const SAVE_VALUE = "__save_key__";
|
||||||
|
|
||||||
export default function ApiKeySelect({ value, onChange, apiKeys = [], cloudEnabled = false, className = "" }) {
|
export default function ApiKeySelect({ value, onChange, apiKeys = [], cloudEnabled = false, className = "" }) {
|
||||||
const isCustom = !apiKeys.some((k) => k.key === value) && value !== "";
|
const [savedKeys, setSavedKeys] = useState([]);
|
||||||
const [mode, setMode] = useState(() => {
|
// Custom mode is sticky once the user types, so an emptied input doesn't jump back to a dropdown option
|
||||||
if (!value) return apiKeys.length > 0 ? apiKeys[0].key : CUSTOM_VALUE;
|
const [customMode, setCustomMode] = useState(false);
|
||||||
if (apiKeys.some((k) => k.key === value)) return value;
|
const [customInput, setCustomInput] = useState("");
|
||||||
return CUSTOM_VALUE;
|
|
||||||
});
|
useEffect(() => {
|
||||||
const [customInput, setCustomInput] = useState(isCustom ? value : "");
|
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 handleSelect = (e) => {
|
||||||
const next = e.target.value;
|
const next = e.target.value;
|
||||||
setMode(next);
|
if (next === SAVE_VALUE) {
|
||||||
|
upsertKeyPreset((value || "").trim());
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (next === CUSTOM_VALUE) {
|
if (next === CUSTOM_VALUE) {
|
||||||
|
setCustomMode(true);
|
||||||
setCustomInput("");
|
setCustomInput("");
|
||||||
onChange("");
|
onChange("");
|
||||||
} else {
|
return;
|
||||||
onChange(next);
|
|
||||||
}
|
}
|
||||||
|
setCustomMode(false);
|
||||||
|
setCustomInput("");
|
||||||
|
const opt = options.find((o) => o.value === next);
|
||||||
|
if (opt) onChange(opt.url ?? opt.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCustomInput = (e) => {
|
const handleCustomInput = (e) => {
|
||||||
const v = e.target.value;
|
const v = e.target.value;
|
||||||
|
setCustomMode(true);
|
||||||
setCustomInput(v);
|
setCustomInput(v);
|
||||||
onChange(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 (
|
return (
|
||||||
<span className={`min-w-0 rounded bg-surface/40 px-2 py-2 text-xs text-text-muted sm:py-1.5 ${className}`}>
|
<span className={`min-w-0 rounded bg-surface/40 px-2 py-2 text-xs text-text-muted sm:py-1.5 ${className}`}>
|
||||||
{cloudEnabled ? "No API keys - Create one in Keys page" : "sk_9router (default)"}
|
{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 (
|
return (
|
||||||
<div className={`flex flex-col gap-1.5 ${className}`}>
|
<div className={`flex flex-col gap-1.5 ${className}`}>
|
||||||
<select
|
<div className="flex items-center gap-2">
|
||||||
value={mode}
|
<select
|
||||||
onChange={handleSelect}
|
value={mode}
|
||||||
className="w-full min-w-0 px-2 py-2 bg-surface rounded text-xs border border-border focus:outline-none focus:ring-1 focus:ring-primary/50 sm:py-1.5"
|
onChange={handleSelect}
|
||||||
>
|
className="flex-1 min-w-0 px-2 py-2 bg-surface rounded text-xs border border-border focus:outline-none focus:ring-1 focus:ring-primary/50 sm:py-1.5"
|
||||||
{apiKeys.map((k) => (
|
>
|
||||||
<option key={k.id} value={k.key}>{k.key}</option>
|
{options.map((o) => (
|
||||||
))}
|
<option key={o.value} value={o.value}>{o.label}</option>
|
||||||
<option value={CUSTOM_VALUE}>Custom...</option>
|
))}
|
||||||
</select>
|
{canSave && <option value={SAVE_VALUE}>+ Save current as...</option>}
|
||||||
{mode === CUSTOM_VALUE && (
|
</select>
|
||||||
|
{isSaved && (
|
||||||
|
<button type="button" onClick={handleDeleteSaved} className="p-1 text-text-muted hover:text-red-500 rounded transition-colors shrink-0" title="Delete saved key">
|
||||||
|
<span className="material-symbols-outlined text-[14px]">delete</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{isCustom && (
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={customInput}
|
value={inputValue}
|
||||||
onChange={handleCustomInput}
|
onChange={handleCustomInput}
|
||||||
placeholder="sk-..."
|
placeholder="sk-..."
|
||||||
className="w-full min-w-0 px-2 py-2 bg-surface rounded border border-border text-xs focus:outline-none focus:ring-1 focus:ring-primary/50 sm:py-1.5"
|
className="w-full min-w-0 px-2 py-2 bg-surface rounded border border-border text-xs focus:outline-none focus:ring-1 focus:ring-primary/50 sm:py-1.5"
|
||||||
|
|||||||
@@ -131,9 +131,9 @@ export default function ClaudeToolCard({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
// Only set selectedApiKey if it exists in apiKeys list
|
// Restore key from settings.json; ApiKeySelect matches it against saved presets
|
||||||
const tokenFromFile = env.ANTHROPIC_AUTH_TOKEN;
|
const tokenFromFile = env.ANTHROPIC_AUTH_TOKEN;
|
||||||
if (tokenFromFile && apiKeys?.some(k => k.key === tokenFromFile)) {
|
if (tokenFromFile) {
|
||||||
setSelectedApiKey(tokenFromFile);
|
setSelectedApiKey(tokenFromFile);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,55 +1,79 @@
|
|||||||
import { UPDATER_CONFIG } from "@/shared/constants/config";
|
import { UPDATER_CONFIG } from "@/shared/constants/config";
|
||||||
|
|
||||||
// Browser-local endpoint presets shared by every CLI tool card
|
// Browser-local preset stores (endpoints, API keys) shared by every CLI tool card
|
||||||
const STORAGE_KEY = "9router.cliToolEndpointPresets";
|
function createStore({ storageKey, changeEvent, itemField, normalize = (v) => v, defaultName = (v) => v }) {
|
||||||
const CHANGE_EVENT = "9router:endpoint-presets-changed";
|
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(/\/+$/, "");
|
const stripSlash = (url) => (url || "").replace(/\/+$/, "");
|
||||||
|
|
||||||
export function readPresets() {
|
const endpoints = createStore({
|
||||||
if (typeof window === "undefined") return [];
|
storageKey: "9router.cliToolEndpointPresets",
|
||||||
try {
|
changeEvent: "9router:endpoint-presets-changed",
|
||||||
const raw = JSON.parse(window.localStorage.getItem(STORAGE_KEY) || "[]");
|
itemField: "baseUrl",
|
||||||
if (!Array.isArray(raw)) return [];
|
normalize: stripSlash,
|
||||||
return raw.filter((p) => p?.name && p?.baseUrl);
|
defaultName: (url) => {
|
||||||
} catch {
|
try { return new URL(url).host; } catch { return url; }
|
||||||
return [];
|
},
|
||||||
}
|
});
|
||||||
}
|
|
||||||
|
|
||||||
function writePresets(presets) {
|
const apiKeys = createStore({
|
||||||
if (typeof window === "undefined") return;
|
storageKey: "9router.cliToolApiKeyPresets",
|
||||||
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(presets));
|
changeEvent: "9router:api-key-presets-changed",
|
||||||
window.dispatchEvent(new CustomEvent(CHANGE_EVENT));
|
itemField: "key",
|
||||||
}
|
});
|
||||||
|
|
||||||
export function subscribePresets(handler) {
|
export const readPresets = endpoints.read;
|
||||||
if (typeof window === "undefined") return () => {};
|
export const subscribePresets = endpoints.subscribe;
|
||||||
window.addEventListener(CHANGE_EVENT, handler);
|
export const upsertPreset = endpoints.upsert;
|
||||||
return () => window.removeEventListener(CHANGE_EVENT, handler);
|
export const deletePreset = endpoints.remove;
|
||||||
}
|
|
||||||
|
|
||||||
function defaultNameFor(url) {
|
export const readKeyPresets = apiKeys.read;
|
||||||
try { return new URL(url).host; } catch { return url; }
|
export const subscribeKeyPresets = apiKeys.subscribe;
|
||||||
}
|
export const upsertKeyPreset = apiKeys.upsert;
|
||||||
|
export const deleteKeyPreset = apiKeys.remove;
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save an applied endpoint unless it exactly matches a built-in dropdown option
|
// Save an applied endpoint unless it exactly matches a built-in dropdown option
|
||||||
export function rememberEndpoint(baseUrl, { tunnelPublicUrl, tailscaleUrl, cloudUrl } = {}) {
|
export function rememberEndpoint(baseUrl, { tunnelPublicUrl, tailscaleUrl, cloudUrl } = {}) {
|
||||||
@@ -64,8 +88,4 @@ export function rememberEndpoint(baseUrl, { tunnelPublicUrl, tailscaleUrl, cloud
|
|||||||
return upsertPreset(url);
|
return upsertPreset(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deletePreset(name) {
|
|
||||||
writePresets(readPresets().filter((p) => p.name !== name));
|
|
||||||
}
|
|
||||||
|
|
||||||
export { stripSlash };
|
export { stripSlash };
|
||||||
@@ -2,17 +2,21 @@
|
|||||||
|
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import PropTypes from "prop-types";
|
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 }) {
|
export default function AddCustomModelModal({ isOpen, providerAlias, providerDisplayAlias, onSave, onClose }) {
|
||||||
const [modelId, setModelId] = useState("");
|
const [modelId, setModelId] = useState("");
|
||||||
|
const [caps, setCaps] = useState(defaultCaps);
|
||||||
const [testStatus, setTestStatus] = useState(null); // null | "testing" | "ok" | "error"
|
const [testStatus, setTestStatus] = useState(null); // null | "testing" | "ok" | "error"
|
||||||
const [testError, setTestError] = useState("");
|
const [testError, setTestError] = useState("");
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
|
|
||||||
// Reset state when modal opens
|
// Reset state when modal opens
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isOpen) { setModelId(""); setTestStatus(null); setTestError(""); }
|
if (isOpen) { setModelId(""); setCaps(defaultCaps()); setTestStatus(null); setTestError(""); }
|
||||||
}, [isOpen]);
|
}, [isOpen]);
|
||||||
|
|
||||||
// Strip provider's own alias prefix (e.g. "cc/model" -> "model" for cc provider)
|
// 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;
|
if (!cleanId || saving) return;
|
||||||
setSaving(true);
|
setSaving(true);
|
||||||
try {
|
try {
|
||||||
await onSave(cleanId);
|
await onSave(cleanId, caps);
|
||||||
} finally {
|
} finally {
|
||||||
setSaving(false);
|
setSaving(false);
|
||||||
}
|
}
|
||||||
@@ -86,6 +90,22 @@ export default function AddCustomModelModal({ isOpen, providerAlias, providerDis
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium mb-1.5 block">Capabilities</label>
|
||||||
|
<div className="flex flex-wrap gap-4">
|
||||||
|
{Object.entries(CAPACITY_META).map(([key, meta]) => (
|
||||||
|
<Toggle
|
||||||
|
key={key}
|
||||||
|
checked={!!caps[key]}
|
||||||
|
onChange={(v) => setCaps((prev) => ({ ...prev, [key]: v }))}
|
||||||
|
label={meta.label}
|
||||||
|
description={meta.desc}
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Test result */}
|
{/* Test result */}
|
||||||
{testStatus === "ok" && (
|
{testStatus === "ok" && (
|
||||||
<div className="flex items-center gap-2 text-sm text-green-600">
|
<div className="flex items-center gap-2 text-sm text-green-600">
|
||||||
|
|||||||
@@ -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 {
|
try {
|
||||||
const res = await fetch("/api/models/custom", {
|
const res = await fetch("/api/models/custom", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
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) {
|
if (res.ok) {
|
||||||
await fetchCustomModels();
|
await fetchCustomModels();
|
||||||
@@ -1006,7 +1006,7 @@ export default function ProviderDetailPage() {
|
|||||||
const isSelected = (connectionId) => selectedConnectionIds.includes(connectionId);
|
const isSelected = (connectionId) => selectedConnectionIds.includes(connectionId);
|
||||||
|
|
||||||
const connectionsList = (
|
const connectionsList = (
|
||||||
<div className="flex min-w-0 flex-col divide-y divide-black/[0.03] dark:divide-white/[0.03]">
|
<div className="flex min-w-0 flex-col divide-y divide-black/[0.03] dark:divide-white/[0.03] max-h-[500px] overflow-y-auto pr-1">
|
||||||
{connections
|
{connections
|
||||||
.map((conn, index) => (
|
.map((conn, index) => (
|
||||||
<div key={conn.id} className="flex min-w-0 items-stretch">
|
<div key={conn.id} className="flex min-w-0 items-stretch">
|
||||||
@@ -1876,8 +1876,8 @@ export default function ProviderDetailPage() {
|
|||||||
isOpen={showAddCustomModel}
|
isOpen={showAddCustomModel}
|
||||||
providerAlias={providerStorageAlias}
|
providerAlias={providerStorageAlias}
|
||||||
providerDisplayAlias={providerDisplayAlias}
|
providerDisplayAlias={providerDisplayAlias}
|
||||||
onSave={async (modelId) => {
|
onSave={async (modelId, caps) => {
|
||||||
await handleAddCustomModel(modelId, "llm", providerStorageAlias);
|
await handleAddCustomModel(modelId, "llm", providerStorageAlias, caps);
|
||||||
setShowAddCustomModel(false);
|
setShowAddCustomModel(false);
|
||||||
}}
|
}}
|
||||||
onClose={() => setShowAddCustomModel(false)}
|
onClose={() => setShowAddCustomModel(false)}
|
||||||
|
|||||||
@@ -434,7 +434,7 @@ export default function ConnectionsCard({ providerId, isOAuth }) {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div className="flex flex-col divide-y divide-black/[0.03] dark:divide-white/[0.03]">
|
<div className="flex flex-col divide-y divide-black/[0.03] dark:divide-white/[0.03] max-h-[500px] overflow-y-auto pr-1">
|
||||||
{connections.map((conn, idx) => (
|
{connections.map((conn, idx) => (
|
||||||
<ConnectionRow
|
<ConnectionRow
|
||||||
key={conn.id}
|
key={conn.id}
|
||||||
|
|||||||
Loaded 50 of 97 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user