diff --git a/CHANGELOG.md b/CHANGELOG.md index fc9153b6ef..ab85227bb1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -82,6 +82,7 @@ _In development — bullets added per PR; finalized at release._ - **fix(providers): model-aware `supportsRedactedThinking` for mixed-format providers** — the redacted-thinking capability was resolved per provider rather than per model, so a mixed-format provider (some models support redacted thinking, others don't) got the wrong answer for some models; the check is now model-aware. ([#4479](https://github.com/diegosouzapw/OmniRoute/pull/4479) — thanks @TF0rd) - **fix(usage): parse numeric-string quota reset timestamps as Unix seconds/ms** — when a provider returned the quota reset timestamp as a numeric string (e.g. `"1700000000"`), `parseResetTime` passed it straight to `new Date(str)`, which returned `Invalid Date` and dropped the reset entirely (UI showed no reset). Numeric strings are now detected and treated as Unix timestamps with the same `< 1e12` seconds-vs-ms heuristic already applied to numeric values; ISO/parseable strings are untouched. Applied symmetrically in `codexUsageQuotas.parseResetTime`. (Inspired by upstream [decolua/9router#768](https://github.com/decolua/9router/pull/768) — thanks @DEYLNN) - **fix(usage): clearer "auth expired" message for Kiro accounts added via Google/GitHub social-auth** — a Kiro account created through the `/api/oauth/kiro/social-exchange` flow (Google or GitHub social login) uses a token format that AWS CodeWhisperer's `GetUsageLimits` quota API frequently rejects with 401/403 even when `/messages` still works. The quota card was throwing the raw upstream error blob (`Failed to fetch Kiro usage: Kiro API error (401): {…}`); social-auth accounts now get the same friendly `Kiro quota API authentication expired. Chat may still work.` message that legacy social-auth users with a stored marker already see, while Builder-ID / IDC accounts keep the existing throw-on-failure behavior so transient upstream errors don't get silently masked. (thanks @anuragg-saxenaa) +- **fix(dashboard): Antigravity CLI Tools model picker now lists API-Key-Compatible custom providers** — the API-Key-compatible / passthrough provider groups in `ModelSelectModal` are derived from the user's `modelAliases`, but `AntigravityToolCard` was the only CLI tool card that didn't fetch `/api/models/alias` or forward the `modelAliases` prop, so a custom OpenAI-compatible provider added in OmniRoute never surfaced in the Antigravity tool's model picker — routing a custom model to Antigravity from there was impossible. The card now mirrors the pattern already used by every sibling tool card (Codex, Claude, Cline, Kilo, Droid, OpenClaw, HermesAgent). (thanks @mxskeen) ### 🔒 Security diff --git a/src/app/(dashboard)/dashboard/cli-code/components/AntigravityToolCard.tsx b/src/app/(dashboard)/dashboard/cli-code/components/AntigravityToolCard.tsx index b385f09385..146b378522 100644 --- a/src/app/(dashboard)/dashboard/cli-code/components/AntigravityToolCard.tsx +++ b/src/app/(dashboard)/dashboard/cli-code/components/AntigravityToolCard.tsx @@ -27,6 +27,10 @@ export default function AntigravityToolCard({ const [modelMappings, setModelMappings] = useState({}); const [modalOpen, setModalOpen] = useState(false); const [currentEditingAlias, setCurrentEditingAlias] = useState(null); + // Model aliases drive the API-Key-compatible / passthrough provider groups in + // ModelSelectModal — without them, custom OpenAI/Anthropic-compatible + // providers don't surface in the picker even when active. + const [modelAliases, setModelAliases] = useState({}); // (#523) Store the key *id* (not the masked string) so the backend can // resolve the real secret from DB before writing to config files. @@ -40,6 +44,7 @@ export default function AntigravityToolCard({ if (isExpanded && !status) { fetchStatus(); loadSavedMappings(); + fetchModelAliases(); } }, [isExpanded, status]); @@ -72,6 +77,16 @@ export default function AntigravityToolCard({ } }; + const fetchModelAliases = async () => { + try { + const res = await fetch("/api/models/alias"); + const data = await res.json(); + if (res.ok) setModelAliases(data.aliases || {}); + } catch (error) { + console.log("Error fetching model aliases:", error); + } + }; + // Windows uses UAC dialog, no sudo needed const isWindows = typeof navigator !== "undefined" && navigator.userAgent?.includes("Windows"); @@ -460,6 +475,7 @@ export default function AntigravityToolCard({ onSelect={handleModelSelect} selectedModel={currentEditingAlias ? modelMappings[currentEditingAlias] : null} activeProviders={activeProviders} + modelAliases={modelAliases} title={t("selectModelForAlias", { alias: currentEditingAlias || "" })} /> diff --git a/tests/unit/ui/AntigravityToolCard-model-aliases.test.tsx b/tests/unit/ui/AntigravityToolCard-model-aliases.test.tsx new file mode 100644 index 0000000000..ed51939819 --- /dev/null +++ b/tests/unit/ui/AntigravityToolCard-model-aliases.test.tsx @@ -0,0 +1,37 @@ +import { describe, expect, it } from "vitest"; +import { readFileSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import { dirname, resolve } from "node:path"; + +// Source-level parity check (#241): AntigravityToolCard must surface +// API-Key-compatible / custom OpenAI-compatible providers in its model picker. +// Those provider groups in are derived from `modelAliases` +// — without the prop, custom-keyed providers are silently hidden even when +// active. The fix mirrors the pattern already used by every sibling CLI tool +// card (Codex, Claude, Cline, Kilo, Droid, OpenClaw, HermesAgent). + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const CARD_PATH = resolve( + __dirname, + "../../../src/app/(dashboard)/dashboard/cli-code/components/AntigravityToolCard.tsx" +); + +describe("AntigravityToolCard model alias wiring", () => { + const source = readFileSync(CARD_PATH, "utf8"); + + it("declares modelAliases state", () => { + expect(source).toMatch(/useState\(\{\}\)/); + expect(source).toMatch(/setModelAliases/); + }); + + it("fetches /api/models/alias when expanded", () => { + expect(source).toContain('fetch("/api/models/alias")'); + expect(source).toMatch(/fetchModelAliases\s*\(\s*\)/); + }); + + it("passes modelAliases prop to ModelSelectModal", () => { + // Regression guard for upstream parity: the prop is what unlocks the + // API-Key-compatible / passthrough provider groups in the picker. + expect(source).toMatch(/modelAliases=\{modelAliases\}/); + }); +});