Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,49 @@
# v0.7.6 (2026-07-25)

## Features
- **Dollar Savings Tracker**: hero card di Overview menampilkan total $ Saved (lifetime) dengan per-mechanism breakdown (RTK/Headroom/Pxpipe/Cache/Caveman/Ponytail) + "With vs Without ExtremeRouter" comparison.
- **Provider Performance Leaderboard**: sortable table di Overview dengan ranking per-provider: Requests, Tokens, TTFT, P95 Latency, Success Rate, Cost. Period selector (24h/7d/30d). Custom provider names resolved dari providerNodes.
- **SSE Live Dashboard**: unified `/api/dashboard/stream` SSE endpoint (stats + breaker + health). Real-time KPI updates via `useDashboardStream` hook dengan auto-reconnect.
- **In-App Notification System**: bell icon di header dengan unread badge, dropdown feed, localStorage history persistence. Push notifications untuk provider down/recovered/health degraded/rate limited.
- **Cross-Transport Fallback**: OpenAI endpoint timeout/5xx → auto-retry Anthropic endpoint (body re-translated). Fresh AbortController per attempt. Applied ke GLM, hcnsec, Bynara, InxoraStudio, CommandCode, Infron, AgentRouter.
- **New Provider: Bynara**: multi-model router (OpenAI + Anthropic + Responses + Image gen/edits).
- **New Provider: InxoraStudio Labs (API)**: OpenAI + Anthropic multi-endpoint.
- **New Provider: InxoraStudio Labs (Web)**: JWT web chat executor (3-step flow), profile badge, auto model discovery.
- **New Provider: Infron AI**: 457+ models, OpenAI + Anthropic, quota tracker (credit balance).
- **New Provider: AgentRouter**: GLM/GPT/Claude, OpenAI + Anthropic, custom pricing model discovery.
- **Updated Provider: Command Code**: migrated dari custom `/alpha/generate` ke standard OpenAI/Anthropic provider API, live model discovery (47 models).
- **Quota Tracker: Grok Web (Subscription)**: SSO cookie rate-limits (Fast/Thinking/Heavy tiers).
- **Quota Tracker: Infron AI**: credit balance via `/v1/balance`.

## Fixes
- **Kiro `REQUEST_BODY_INVALID`**: normalize `(level)` suffix sebelum resolving synthetic variants. Map explicit levels ke native Kiro effort fields hanya untuk supported families (Claude 5 / GPT-5.6). `thinkingLevels.js` server-side gating.
- **Responses API accumulator**: shared `ResponsesAccumulator` untuk streaming + forced non-stream. Alias-safe tool reconstruction. `preferComplete` untuk snapshot merge. Exactly-once terminal semantics.
- **Responses `incomplete`/`cancelled`**: terminal events sekarang di-recognize di `responsesStreamHelpers.js`.
- **Antigravity "model turn" 400**: strip trailing `role:"model"` turns dari `contents[]` sebelum kirim ke Google Cloud Code.
- **Antigravity tier routing**: `gemini-3.6-flash-high/medium/low` dengan `upstreamModelId: "gemini-3.6-flash-tiered(high)"` + parser di `getModelUpstreamId`.
- **Cloud Code endpoint isolation**: gemini-cli→`cloudcode-pa`, antigravity→`daily-cloudcode-pa` via per-provider `CLOUD_CODE_API` map.
- **Circuit breaker `releaseBreakerProbe`**: `monitors`→`breakers` ReferenceError fix.
- **Combo `comboStickyLimit`**: wrong key `comboStickyLimit`→`comboStickyRoundRobinLimit`.
- **Combo `comboStrategies` lost-update**: deep-merge per combo-name + null delete-signal.
- **Cross-transport abort**: fresh AbortController untuk alternate attempt.
- **Cross-transport error logging**: alternate failure error tidak lagi di-swallow silently.
- **Responses output getter**: dedup alias-registered tools + iterate string-keyed items.
- **Notification spam**: `health_update` dan `usage_update` tidak lagi jadi notification.
- **Notification raw JSON**: `formatEventMessage` return `null` untuk unknown types.
- **Notification duplicate**: `lastProcessedTs` useRef guard + dedup.
- **NotificationBell setState-during-render**: `markAllRead` deferred via `requestAnimationFrame`.
- **NotificationBell key collision**: `idCounter` seeded dari `Date.now()` + max history id.
- **hcnsec model discovery**: tambah `"openai"` filter + authenticated suggested-models proxy.
- **WEB_COOKIE_PROVIDERS modelsFetcher**: page.js sekarang include webCookie providers di model discovery chain.
- **InxoraStudio profile badge**: `InxoraProfile` component + wiring di ConnectionsCard.

## Improvements
- **Dead code cleanup**: hapus 12 dead files (1,705 LOC) + dead `convertResponsesApiFormat` function.
- **Junk dependency removed**: `fs` (0.0.1-security placeholder package) dari dependencies.
- **Command Code executor**: hapus custom `CommandCodeExecutor`, pakai `DefaultExecutor` (OpenAI/Anthropic native).
- **Model discovery**: `suggested-models` route sekarang support `connectionId` untuk authenticated discovery.
- **Validate route**: hcnsec, InxoraStudio-web, inxorastudio (API) validate cases.

# v0.7.5 (2026-07-22)

## Features
Expand Down
2 changes: 1 addition & 1 deletion cli/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@rsalmn/extremerouter",
"version": "0.7.5",
"version": "0.7.6",
"description": "ExtremeRouter CLI - AI Gateway control plane",
"bin": {
"extremerouter": "./cli.js"
Expand Down
22 changes: 19 additions & 3 deletions open-sse/config/appConstants.js
Original file line number Diff line number Diff line change
Expand Up @@ -132,12 +132,28 @@ export const ANTIGRAVITY_HEADERS = {
"User-Agent": `antigravity/2.1.1 ${platform()}/${arch()}`
};

// Cloud Code Assist API
// Cloud Code Assist API endpoints differ by client ecosystem.
// gemini-cli uses the production host (cloudcode-pa.googleapis.com) while
// antigravity uses the sandbox host (daily-cloudcode-pa.googleapis.com) per
// decolua/9router commit 190020c. Keyed by provider id; projectId.js resolves
// the right endpoints per-connection.
export const CLOUD_CODE_API = {
loadCodeAssist: "https://cloudcode-pa.googleapis.com/v1internal:loadCodeAssist",
onboardUser: "https://cloudcode-pa.googleapis.com/v1internal:onboardUser",
"gemini-cli": {
loadCodeAssist: "https://cloudcode-pa.googleapis.com/v1internal:loadCodeAssist",
onboardUser: "https://cloudcode-pa.googleapis.com/v1internal:onboardUser",
},
antigravity: {
loadCodeAssist: "https://daily-cloudcode-pa.googleapis.com/v1internal:loadCodeAssist",
onboardUser: "https://daily-cloudcode-pa.googleapis.com/v1internal:onboardUser",
},
};

// Back-compat flat accessor: callers that haven't been migrated to pass a
// provider default to the gemini-cli (production) endpoints — the previous
// behavior before this map was split.
export const CLOUD_CODE_LOAD_CODEASSIST = CLOUD_CODE_API["gemini-cli"].loadCodeAssist;
export const CLOUD_CODE_ONBOARD_USER = CLOUD_CODE_API["gemini-cli"].onboardUser;

export const LOAD_CODE_ASSIST_HEADERS = {
"Content-Type": "application/json",
"User-Agent": "google-api-nodejs-client/9.15.1",
Expand Down
78 changes: 78 additions & 0 deletions open-sse/config/kiroConstants.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
*/

import { extractThinking } from "../translator/concerns/thinkingUnified.js";
import { parseSuffix } from "../translator/concerns/thinkingUnified.js";
import { effortToBudget } from "../translator/concerns/thinking.js";

export const KIRO_AGENTIC_SUFFIX = "-agentic";
Expand Down Expand Up @@ -234,6 +235,83 @@ export function resolveKiroModel(model) {
return { upstream, agentic, thinking };
}

/**
* Resolve the per-family effort path for a Kiro upstream model id.
*
* Kiro upstream (CodeWhisperer) accepts native effort fields ONLY for specific
* model families. Other families (legacy Claude 4.5, GLM, DeepSeek, Qwen,
* MiniMax) reject native effort as REQUEST_BODY_INVALID — their reasoning flows
* solely via the `<thinking_mode>` system tag injection.
*
* Returns one of:
* - "kiro-claude" → Claude 5 family (Sonnet 5, Haiku 5, Opus 5)
* - "kiro-gpt" → GPT-5.6 family (Sol, Terra, Luna)
* - null → not supported (tag-only reasoning)
*
* @param {string} upstreamModel - clean upstream id (after resolveKiroModel)
* @returns {string|null}
*/
export function resolveKiroEffortPath(upstreamModel) {
if (typeof upstreamModel !== "string" || !upstreamModel) return null;
const lower = upstreamModel.toLowerCase();
// Claude 5 family (NOT Claude 4.x — those are legacy and reject native effort).
// Match claude-{opus,sonnet,haiku}-5 explicitly to exclude 4.5/4.6/etc.
if (/^claude-(opus|sonnet|haiku)-5(\b|$|-)/.test(lower)) return "kiro-claude";
// GPT-5.6 family.
if (/^gpt-5\.6-/.test(lower)) return "kiro-gpt";
return null;
}

/**
* Resolve a Kiro model id after consuming the generic `model(level)` suffix
* that the dashboard thinking-level picker appends.
*
* The suffix is an ExtremeRouter request override (parsed by parseSuffix),
* NOT part of Kiro's upstream model id. We strip it before resolving synthetic
* variants (-thinking / -agentic) so the upstream modelId stays clean.
*
* @param {string} model
* @returns {{ model: string, upstream: string, agentic: boolean, thinking: boolean, thinkingOverride: object|null }}
*/
export function resolveKiroModelIntent(model) {
const { cleanModel, override } = parseSuffix(model);
return {
model: cleanModel,
...resolveKiroModel(cleanModel),
thinkingOverride: override,
};
}

/**
* Apply a parsed `model(level)` override to a request body without mutating the
* caller's body. Translates the override into the appropriate thinking shape so
* downstream consumers (resolveKiroThinkingBudget, the system-tag injector) see
* a consistent intent regardless of whether it came from the body fields or the
* model-name suffix.
*
* @param {object} body
* @param {object|null} override - { mode: "level"|"budget"|"auto"|"none", ... }
* @returns {object} new body with thinking fields set
*/
export function applyKiroThinkingOverride(body, override) {
if (!override) return body;

const next = { ...body };
if (override.mode === "budget") {
delete next.output_config;
delete next.reasoning_effort;
delete next.reasoning;
next.thinking = { type: "enabled", budget_tokens: override.budget };
return next;
}

next.output_config = {
...(body.output_config || {}),
effort: override.mode === "level" ? override.level : override.mode,
};
return next;
}

/**
* Build the magic system-prompt prefix that turns Kiro reasoning on.
* Same shape as CLIProxyAPIPlus.
Expand Down
19 changes: 18 additions & 1 deletion open-sse/config/providerModels.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,24 @@ export function getModelType(aliasOrId, modelId) {
export function getModelUpstreamId(aliasOrId, modelId) {
const models = PROVIDER_MODELS[aliasOrId];
const found = models?.find(m => m.id === modelId);
if (found?.upstreamModelId) return found.upstreamModelId;
if (found?.upstreamModelId) {
// Tier-preset suffix support: a model entry can carry a preset tag in its
// upstreamModelId, e.g. "gemini-3.6-flash-tiered(high)" for tiered routing.
// When the caller appended an effort/tier suffix (e.g. "-high"), we merge
// the caller's suffix with the preset; otherwise the preset wins on its own.
// Port of decolua/9router commit 190020c (isolates tiered model routing).
const resolvedId = found.upstreamModelId;
const presetMatch = resolvedId.match(/\([^()]+\)\s*$/);
const presetSuffix = presetMatch?.[0] || "";
if (presetSuffix) {
const resolvedBase = resolvedId.slice(0, presetMatch.index).trim();
// Caller suffix (passed as modelId tail) takes precedence if present.
const callerSuffixMatch = typeof modelId === "string" ? modelId.match(/-(high|medium|low)$/i) : null;
const callerTier = callerSuffixMatch?.[0] ? `(${callerSuffixMatch[0].slice(1).toLowerCase()})` : "";
return resolvedBase + (callerTier || presetSuffix);
}
return resolvedId;
}
if (aliasOrId === "cx" && typeof modelId === "string" && modelId.endsWith(CODEX_REVIEW_SUFFIX)) {
return modelId.slice(0, -CODEX_REVIEW_SUFFIX.length);
}
Expand Down
11 changes: 11 additions & 0 deletions open-sse/executors/antigravity.js
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,17 @@ export class AntigravityExecutor extends BaseExecutor {
return c;
});

// Google Cloud Code rejects requests where the last content turn is from
// the model ("Requests ending with a model turn are not supported"). This
// happens when the client sends a conversation history that ends with an
// assistant message (e.g. Claude Code sending the previous response as
// context). Strip trailing model turns so the last entry is always user.
if (Array.isArray(contents) && contents.length > 0) {
while (contents.length > 1 && contents[contents.length - 1]?.role === "model") {
contents.pop();
}
}

// Sanitize tool schemas and function names before sending to Antigravity.
let tools = body.request?.tools;

Expand Down
8 changes: 5 additions & 3 deletions open-sse/executors/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import { OpenCodeGoExecutor } from "./opencode-go.js";
import { GrokWebExecutor } from "./grok-web.js";
import { PerplexityWebExecutor } from "./perplexity-web.js";
import { OllamaLocalExecutor } from "./ollama-local.js";
import { CommandCodeExecutor } from "./commandcode.js";
import { XiaomiTokenplanExecutor } from "./xiaomi-tokenplan.js";
import { MimoFreeExecutor } from "./mimo-free.js";
import { CodeBuddyExecutor } from "./codebuddy-cn.js";
Expand All @@ -30,6 +29,7 @@ import { BlackboxWebExecutor } from "./blackbox-web.js";
import { ZenmuxFreeExecutor } from "./zenmux-free.js";
import { ApiAirforceExecutor } from "./api-airforce.js";
import { FreeBuffWebExecutor } from "./freebuff-web.js";
import { InxorastudioWebExecutor } from "./inxorastudio-web.js";
import { PerplexityAgentExecutor } from "./perplexity-agent.js";
import { QwenCloudExecutor } from "./qwencloud.js";
import { T3ChatWebExecutor } from "./t3-web.js";
Expand Down Expand Up @@ -73,7 +73,7 @@ const executors = {
"grok-web": new GrokWebExecutor(),
"perplexity-web": new PerplexityWebExecutor(),
"ollama-local": new OllamaLocalExecutor(),
commandcode: new CommandCodeExecutor(),
// commandcode now uses DefaultExecutor (OpenAI/Anthropic native endpoints)
"xiaomi-tokenplan": new XiaomiTokenplanExecutor(),
"mimo-free": new MimoFreeExecutor(),
mmf: new MimoFreeExecutor(), // Alias for mimo-free
Expand Down Expand Up @@ -103,6 +103,7 @@ const executors = {
"zenmux-free": new ZenmuxFreeExecutor(),
"api-airforce": new ApiAirforceExecutor(),
"freebuff-web": new FreeBuffWebExecutor(),
"inxorastudio-web": new InxorastudioWebExecutor(),
"perplexity-agent": new PerplexityAgentExecutor(),
"qwencloud": new QwenCloudExecutor(),
lmarena: new LMArenaExecutor(),
Expand Down Expand Up @@ -142,7 +143,7 @@ export { OpenCodeGoExecutor } from "./opencode-go.js";
export { GrokWebExecutor } from "./grok-web.js";
export { PerplexityWebExecutor } from "./perplexity-web.js";
export { OllamaLocalExecutor } from "./ollama-local.js";
export { CommandCodeExecutor } from "./commandcode.js";
// CommandCodeExecutor removed — provider now uses standard OpenAI/Anthropic endpoints (DefaultExecutor)
export { XiaomiTokenplanExecutor } from "./xiaomi-tokenplan.js";
export { MimoFreeExecutor } from "./mimo-free.js";
export { CodeBuddyExecutor } from "./codebuddy-cn.js";
Expand Down Expand Up @@ -171,6 +172,7 @@ export { HuggingChatExecutor } from "./huggingchat.js";
export { ZenmuxFreeExecutor } from "./zenmux-free.js";
export { ApiAirforceExecutor } from "./api-airforce.js";
export { FreeBuffWebExecutor } from "./freebuff-web.js";
export { InxorastudioWebExecutor } from "./inxorastudio-web.js";
export { PerplexityAgentExecutor } from "./perplexity-agent.js";
export { QwenCloudExecutor } from "./qwencloud.js";
export { LMArenaExecutor } from "./lmarena.js";
Expand Down
Loading