fix(scripts): ai

This commit is contained in:
null 2026-05-21 03:29:35 -05:00
parent 1a0eaeee68
commit a6c24673bc
11 changed files with 446 additions and 53 deletions

View File

@ -25,6 +25,7 @@ from app.schemas.provider_credentials import (
ProviderCredentialUpdate,
ProviderUsageLiveRead,
RequestWindowRead,
SubscriptionWindowRead,
TokenWindowRead,
)
from app.services.provider_usage import fetch_provider_usage
@ -48,6 +49,8 @@ def _to_read(cred: ProviderCredential) -> ProviderCredentialRead:
display_name=cred.display_name,
api_key_last_four=cred.api_key_last_four,
has_api_key=bool(cred.api_key),
session_key_last_four=cred.session_key_last_four,
has_session_key=bool(cred.session_key),
base_url=cred.base_url,
active=cred.active,
created_at=cred.created_at,
@ -101,6 +104,7 @@ async def create_provider_credential(
)
last_four = payload.api_key[-4:] if payload.api_key and len(payload.api_key) >= 4 else None
sk_last_four = payload.session_key[-4:] if payload.session_key and len(payload.session_key) >= 4 else None
cred = ProviderCredential(
organization_id=ctx.organization.id,
provider=payload.provider,
@ -108,6 +112,8 @@ async def create_provider_credential(
display_name=payload.display_name or payload.account_key,
api_key=payload.api_key or None,
api_key_last_four=last_four,
session_key=payload.session_key or None,
session_key_last_four=sk_last_four,
base_url=payload.base_url or None,
active=payload.active,
)
@ -169,6 +175,16 @@ async def test_provider_credential(
input_tokens=_tok(live.input_tokens),
output_tokens=_tok(live.output_tokens),
requests=_req(live.requests),
subscription_windows=[
SubscriptionWindowRead(
key=w.key,
label=w.label,
pct_used=w.pct_used,
reset_in_ms=w.reset_in_ms,
)
for w in live.subscription_windows
],
subscription_plan=live.subscription_plan,
models=live.models,
sample_model=live.sample_model,
sample_input_tokens=live.sample_input_tokens,
@ -215,6 +231,13 @@ async def update_provider_credential(
else:
cred.api_key = payload.api_key
cred.api_key_last_four = payload.api_key[-4:] if len(payload.api_key) >= 4 else None
if payload.session_key is not None:
if payload.session_key == "":
cred.session_key = None
cred.session_key_last_four = None
else:
cred.session_key = payload.session_key
cred.session_key_last_four = payload.session_key[-4:] if len(payload.session_key) >= 4 else None
cred.updated_at = utcnow()
await crud.save(session, cred)
@ -244,6 +267,7 @@ async def get_provider_usage_live(
account_key=cred.account_key,
api_key=cred.api_key,
base_url=cred.base_url,
session_key=cred.session_key,
force_refresh=refresh,
)
@ -274,6 +298,16 @@ async def get_provider_usage_live(
input_tokens=_tok(live.input_tokens),
output_tokens=_tok(live.output_tokens),
requests=_req(live.requests),
subscription_windows=[
SubscriptionWindowRead(
key=w.key,
label=w.label,
pct_used=w.pct_used,
reset_in_ms=w.reset_in_ms,
)
for w in live.subscription_windows
],
subscription_plan=live.subscription_plan,
models=live.models,
sample_model=live.sample_model,
sample_input_tokens=live.sample_input_tokens,

View File

@ -42,6 +42,14 @@ class ProviderCredential(QueryModel, table=True):
api_key_last_four: str | None = Field(default=None) # shown in UI for verification
base_url: str | None = Field(default=None) # Ollama, Azure, custom endpoints
# Session / OAuth token for subscription-usage endpoints.
# Anthropic: claude.ai sessionKey cookie (sk-ant-sid-...)
# OpenAI Codex: ChatGPT bearer token from browser session
# This is a different credential from the API key — it authenticates against
# the provider's subscription system rather than the generation API.
session_key: str | None = Field(default=None) # full token — never returned in API
session_key_last_four: str | None = Field(default=None)
active: bool = Field(default=True, index=True)
created_at: datetime = Field(default_factory=utcnow)

View File

@ -15,6 +15,7 @@ class ProviderCredentialCreate(SQLModel):
account_key: str
display_name: str = ""
api_key: str | None = None
session_key: str | None = None # Claude.ai / ChatGPT session token for subscription usage
base_url: str | None = None
active: bool = True
@ -23,16 +24,29 @@ class ProviderCredentialTestRequest(SQLModel):
provider: str
account_key: str = "test"
api_key: str | None = None
session_key: str | None = None
base_url: str | None = None
class ProviderCredentialUpdate(SQLModel):
display_name: str | None = None
api_key: str | None = None # None = keep existing; "" = clear it
api_key: str | None = None # None = keep existing; "" = clear it
session_key: str | None = None # None = keep existing; "" = clear it
base_url: str | None = None
active: bool | None = None
class SubscriptionWindowRead(SQLModel):
"""One subscription-plan usage window from a session/OAuth token."""
key: str # "five_hour" | "seven_day" | "seven_day_sonnet" | etc.
label: str # "Current session" | "All models" | "Sonnet" | "3h" | "Week"
pct_used: float # 0100
reset_in_ms: int | None = None
source: str = "provider_native"
confidence: str = "high"
class TokenWindowRead(SQLModel):
limit: int | None = None
remaining: int | None = None
@ -63,6 +77,9 @@ class ProviderUsageLiveRead(SQLModel):
input_tokens: TokenWindowRead # Anthropic input-only window
output_tokens: TokenWindowRead # Anthropic output-only window
requests: RequestWindowRead
# Subscription usage windows — populated when session_key is configured
subscription_windows: list[SubscriptionWindowRead] = []
subscription_plan: str | None = None # e.g. "pro", "plus ($15.00)"
models: list[str] = []
sample_model: str | None = None
sample_input_tokens: int | None = None
@ -73,7 +90,7 @@ class ProviderUsageLiveRead(SQLModel):
class ProviderCredentialRead(SQLModel):
"""Safe read schema — api_key is never included."""
"""Safe read schema — api_key and session_key are never included."""
id: UUID
organization_id: UUID
@ -82,6 +99,8 @@ class ProviderCredentialRead(SQLModel):
display_name: str
api_key_last_four: str | None
has_api_key: bool
session_key_last_four: str | None = None
has_session_key: bool = False
base_url: str | None
active: bool
created_at: datetime

View File

@ -98,6 +98,23 @@ class RequestWindow:
return max(0, int(delta * 1000))
@dataclass
class SubscriptionWindow:
"""One subscription-plan usage window (e.g. 5h session, 7-day all-models)."""
key: str # "five_hour" | "seven_day" | "seven_day_sonnet" | "seven_day_opus"
label: str # human label: "Current session" | "All models" | "Sonnet" | "Opus"
pct_used: float # 0100
reset_at: datetime | None = None # UTC naive datetime
@property
def reset_in_ms(self) -> int | None:
if self.reset_at is None:
return None
delta = (self.reset_at - utcnow()).total_seconds()
return max(0, int(delta * 1000))
@dataclass
class ProviderUsageLive:
provider: str
@ -113,6 +130,9 @@ class ProviderUsageLive:
input_tokens: TokenWindow = field(default_factory=TokenWindow) # Anthropic input-only window
output_tokens: TokenWindow = field(default_factory=TokenWindow) # Anthropic output-only window
requests: RequestWindow = field(default_factory=RequestWindow)
# Provider subscription windows — populated when session_key is provided
subscription_windows: list[SubscriptionWindow] = field(default_factory=list)
subscription_plan: str | None = None # e.g. "pro", "plus ($15.00)"
models: list[str] = field(default_factory=list) # model IDs available on this key
raw_headers: dict[str, str] = field(default_factory=dict)
sample_model: str | None = None
@ -612,6 +632,174 @@ async def _fetch_ollama(base_url: str | None, api_key: str | None) -> ProviderUs
return result
# ---------------------------------------------------------------------------
# Subscription usage fetchers — require session/OAuth tokens, not API keys
# ---------------------------------------------------------------------------
_ANTHROPIC_SUBSCRIPTION_URL = "https://api.anthropic.com/api/oauth/usage"
_CODEX_SUBSCRIPTION_URL = "https://chatgpt.com/backend-api/wham/usage"
_ANTHROPIC_WINDOW_LABELS: dict[str, str] = {
"five_hour": "Current session",
"seven_day": "All models",
"seven_day_sonnet": "Sonnet",
"seven_day_opus": "Opus",
}
async def _fetch_anthropic_subscription(session_key: str) -> list[SubscriptionWindow]:
"""Fetch Claude subscription usage windows via the Anthropic OAuth usage endpoint.
Requires a Claude.ai session key (the ``sessionKey`` cookie value, which
starts with ``sk-ant-``). Returns an empty list if the key is invalid or
the endpoint is unreachable.
"""
async with httpx.AsyncClient(timeout=REQUEST_TIMEOUT) as client:
try:
resp = await client.get(
_ANTHROPIC_SUBSCRIPTION_URL,
headers={
"Authorization": f"Bearer {session_key}",
"User-Agent": "pipeline",
"Accept": "application/json",
"anthropic-version": "2023-06-01",
"anthropic-beta": "oauth-2025-04-20",
},
)
except Exception as exc:
logger.warning("provider_usage.subscription.anthropic.fetch_failed error=%s", exc)
return []
if not resp.status_code == 200:
logger.debug(
"provider_usage.subscription.anthropic.http_error status=%s body=%s",
resp.status_code,
resp.text[:200],
)
return []
try:
data = resp.json()
except Exception:
return []
windows: list[SubscriptionWindow] = []
for key, label in _ANTHROPIC_WINDOW_LABELS.items():
block = data.get(key)
if not isinstance(block, dict):
continue
utilization = block.get("utilization")
if utilization is None:
continue
resets_at_str = block.get("resets_at")
reset_dt: datetime | None = None
if isinstance(resets_at_str, str):
try:
normalized = resets_at_str.replace("Z", "+00:00")
parsed = datetime.fromisoformat(normalized)
if parsed.tzinfo is not None:
reset_dt = parsed.astimezone(timezone.utc).replace(tzinfo=None)
else:
reset_dt = parsed
except ValueError:
pass
windows.append(SubscriptionWindow(
key=key,
label=label,
pct_used=round(float(utilization) * 100, 1),
reset_at=reset_dt,
))
logger.info(
"provider_usage.subscription.anthropic.fetched windows=%d",
len(windows),
)
return windows
async def _fetch_codex_subscription(session_key: str) -> tuple[list[SubscriptionWindow], str | None]:
"""Fetch Codex/ChatGPT subscription usage windows via the wham/usage endpoint.
Requires a ChatGPT bearer token (from browser session, not the standard
API key). Returns (windows, plan_label) or ([], None) on failure.
"""
async with httpx.AsyncClient(timeout=REQUEST_TIMEOUT) as client:
try:
resp = await client.get(
_CODEX_SUBSCRIPTION_URL,
headers={
"Authorization": f"Bearer {session_key}",
"Accept": "application/json",
},
)
except Exception as exc:
logger.warning("provider_usage.subscription.codex.fetch_failed error=%s", exc)
return [], None
if resp.status_code != 200:
logger.debug(
"provider_usage.subscription.codex.http_error status=%s",
resp.status_code,
)
return [], None
try:
data = resp.json()
except Exception:
return [], None
windows: list[SubscriptionWindow] = []
rate_limit = data.get("rate_limit") or {}
primary = rate_limit.get("primary_window")
if isinstance(primary, dict) and primary.get("used_percent") is not None:
window_seconds = primary.get("limit_window_seconds") or 10800
window_hours = round(window_seconds / 3600)
reset_ts = primary.get("reset_at")
reset_dt: datetime | None = None
if isinstance(reset_ts, (int, float)):
reset_dt = datetime.fromtimestamp(reset_ts, tz=timezone.utc).replace(tzinfo=None)
windows.append(SubscriptionWindow(
key="primary",
label=f"{window_hours}h",
pct_used=round(float(primary["used_percent"]), 1),
reset_at=reset_dt,
))
secondary = rate_limit.get("secondary_window")
if isinstance(secondary, dict) and secondary.get("used_percent") is not None:
sec_seconds = secondary.get("limit_window_seconds") or 86400
sec_hours = round(sec_seconds / 3600)
sec_label = "Week" if sec_hours >= 168 else f"{sec_hours}h"
reset_ts = secondary.get("reset_at")
reset_dt = None
if isinstance(reset_ts, (int, float)):
reset_dt = datetime.fromtimestamp(reset_ts, tz=timezone.utc).replace(tzinfo=None)
windows.append(SubscriptionWindow(
key="secondary",
label=sec_label,
pct_used=round(float(secondary["used_percent"]), 1),
reset_at=reset_dt,
))
# Plan label
plan_type = data.get("plan_type")
credits = data.get("credits") or {}
balance = credits.get("balance")
plan_label: str | None = None
if isinstance(balance, (int, float)) and balance > 0:
plan_label = f"{plan_type} (${float(balance):.2f})" if plan_type else f"${float(balance):.2f}"
elif plan_type:
plan_label = str(plan_type)
logger.info(
"provider_usage.subscription.codex.fetched windows=%d plan=%s",
len(windows),
plan_label,
)
return windows, plan_label
# ---------------------------------------------------------------------------
# In-memory TTL cache
# ---------------------------------------------------------------------------
@ -645,10 +833,15 @@ async def fetch_provider_usage(
api_key: str | None,
base_url: str | None,
*,
session_key: str | None = None,
force_refresh: bool = False,
) -> ProviderUsageLive:
"""Fetch live usage from the provider API.
When ``session_key`` is provided, also fetches subscription-plan usage
windows (e.g. "Current session 77% used, resets in 23 min") in addition
to the standard API rate-limit diagnostics.
Results are cached for CACHE_TTL_SECONDS. Pass force_refresh=True to
bypass the cache (e.g., when the user clicks Refresh).
"""
@ -658,24 +851,50 @@ async def fetch_provider_usage(
return cached
if provider == "anthropic":
if not api_key:
if not api_key and not session_key:
result = ProviderUsageLive(
provider=provider, account_key=account_key,
checked_at=utcnow(), reachable=False,
error="No API key configured.",
)
else:
elif api_key:
result = await _fetch_anthropic(api_key, base_url)
else:
# session_key only — can still get subscription data, mark as reachable
result = ProviderUsageLive(
provider=provider, account_key=account_key,
checked_at=utcnow(), reachable=True,
)
# Overlay subscription windows when session_key available
if session_key and result.reachable is not False:
sub_windows = await _fetch_anthropic_subscription(session_key)
if sub_windows:
result.subscription_windows = sub_windows
result.reachable = True
elif not result.reachable:
result.error = result.error or "No subscription data returned."
elif provider in ("openai", "codex"):
if not api_key:
if not api_key and not session_key:
result = ProviderUsageLive(
provider=provider, account_key=account_key,
checked_at=utcnow(), reachable=False,
error="No API key configured.",
)
else:
elif api_key:
result = await _fetch_openai(api_key, base_url)
else:
result = ProviderUsageLive(
provider=provider, account_key=account_key,
checked_at=utcnow(), reachable=True,
)
# Overlay subscription windows when session_key available
if session_key and result.reachable is not False:
sub_windows, plan_label = await _fetch_codex_subscription(session_key)
if sub_windows:
result.subscription_windows = sub_windows
result.subscription_plan = plan_label
result.reachable = True
elif provider == "ollama":
result = await _fetch_ollama(base_url, api_key)

View File

@ -0,0 +1,26 @@
"""Add session_key and session_key_last_four to provider_credentials.
Revision ID: a1b2c3d4e5f9
Revises: f0a1b2c3d4e5
Create Date: 2026-05-21
"""
from __future__ import annotations
import sqlalchemy as sa
from alembic import op
revision = "a1b2c3d4e5f9"
down_revision = "f0a1b2c3d4e5"
branch_labels = None
depends_on = None
def upgrade() -> None:
op.add_column("provider_credentials", sa.Column("session_key", sa.String(), nullable=True))
op.add_column("provider_credentials", sa.Column("session_key_last_four", sa.String(), nullable=True))
def downgrade() -> None:
op.drop_column("provider_credentials", "session_key_last_four")
op.drop_column("provider_credentials", "session_key")

View File

@ -258,6 +258,7 @@ export * from "./skillPackRead";
export * from "./skillPackReadMetadata";
export * from "./skillPackSyncResponse";
export * from "./soulsDirectoryMarkdownResponse";
export * from "./subscriptionWindowRead";
export * from "./soulsDirectorySearchResponse";
export * from "./soulsDirectorySoulRef";
export * from "./soulUpdateRequest";

View File

@ -10,6 +10,7 @@ export interface ProviderCredentialCreate {
account_key: string;
display_name?: string;
api_key?: string | null;
session_key?: string | null;
base_url?: string | null;
active?: boolean;
}

View File

@ -16,6 +16,8 @@ export interface ProviderCredentialRead {
display_name: string;
api_key_last_four: string | null;
has_api_key: boolean;
session_key_last_four?: string | null;
has_session_key?: boolean;
base_url: string | null;
active: boolean;
created_at: string;

View File

@ -5,6 +5,7 @@
* OpenAPI spec version: 0.1.0
*/
import type { RequestWindowRead } from "./requestWindowRead";
import type { SubscriptionWindowRead } from "./subscriptionWindowRead";
import type { TokenWindowRead } from "./tokenWindowRead";
/**
@ -20,7 +21,10 @@ export interface ProviderUsageLiveRead {
error?: string | null;
tokens: TokenWindowRead;
input_tokens: TokenWindowRead;
output_tokens: TokenWindowRead;
requests: RequestWindowRead;
subscription_windows: SubscriptionWindowRead[];
subscription_plan?: string | null;
models?: string[];
sample_model?: string | null;
sample_input_tokens?: number | null;

View File

@ -0,0 +1,18 @@
/**
* Generated by orval v8.3.0 🍺
* Do not edit manually.
* Mission Control API
* OpenAPI spec version: 0.1.0
*/
/**
* One subscription-plan usage window from a session/OAuth token.
*/
export interface SubscriptionWindowRead {
key: string;
label: string;
pct_used: number;
reset_in_ms?: number | null;
source?: string;
confidence?: string;
}

View File

@ -32,10 +32,13 @@ const PROVIDERS = [
icon: Bot,
description: "Anthropic Claude API key. Add multiple accounts to track separate keys or billing profiles.",
keyLabel: "API Key",
keyPlaceholder: "sk-ant-…",
keyPlaceholder: "sk-ant-api03-…",
showBaseUrl: false,
allowMultiple: true,
accountKeyDefault: "default",
showSessionKey: true,
sessionKeyLabel: "Claude.ai session key (for subscription usage)",
sessionKeyPlaceholder: "sk-ant-sid-… (from claude.ai cookie)",
},
{
id: "openai",
@ -43,10 +46,13 @@ const PROVIDERS = [
icon: Bot,
description: "OpenAI API key. Add multiple accounts to track usage separately.",
keyLabel: "API Key",
keyPlaceholder: "sk-…",
keyPlaceholder: "sk-proj-…",
showBaseUrl: false,
allowMultiple: true,
accountKeyDefault: "",
showSessionKey: true,
sessionKeyLabel: "ChatGPT session token (for subscription usage)",
sessionKeyPlaceholder: "Bearer token from browser session",
},
{
id: "ollama",
@ -58,6 +64,9 @@ const PROVIDERS = [
showBaseUrl: true,
allowMultiple: true,
accountKeyDefault: "default",
showSessionKey: false,
sessionKeyLabel: "",
sessionKeyPlaceholder: "",
},
] as const;
@ -71,6 +80,9 @@ interface CredentialFormProps {
providerId: ProviderId;
allowMultiple: boolean;
showBaseUrl: boolean;
showSessionKey: boolean;
sessionKeyLabel: string;
sessionKeyPlaceholder: string;
keyLabel: string;
keyPlaceholder: string;
accountKeyDefault: string;
@ -78,6 +90,7 @@ interface CredentialFormProps {
account_key: string;
display_name: string;
api_key: string;
session_key: string;
base_url: string;
}) => Promise<void>;
onTest: (data: {
@ -93,6 +106,9 @@ interface CredentialFormProps {
function CredentialForm({
allowMultiple,
showBaseUrl,
showSessionKey,
sessionKeyLabel,
sessionKeyPlaceholder,
keyLabel,
keyPlaceholder,
accountKeyDefault,
@ -105,6 +121,7 @@ function CredentialForm({
const [accountKey, setAccountKey] = useState(accountKeyDefault);
const [displayName, setDisplayName] = useState("");
const [apiKey, setApiKey] = useState("");
const [sessionKey, setSessionKey] = useState("");
const [baseUrl, setBaseUrl] = useState("");
const [isTesting, setIsTesting] = useState(false);
const [testResult, setTestResult] = useState<ProviderUsageLiveRead | null>(null);
@ -185,6 +202,21 @@ function CredentialForm({
autoComplete="new-password"
/>
</div>
{showSessionKey && (
<div>
<label className="mb-1 block text-xs font-medium text-muted">
{sessionKeyLabel}
</label>
<Input
type="password"
value={sessionKey}
onChange={(e) => setSessionKey(e.target.value)}
placeholder={sessionKeyPlaceholder}
disabled={isSaving}
autoComplete="new-password"
/>
</div>
)}
{error && (
<p className="text-sm text-[color:var(--danger)]">{error}</p>
)}
@ -206,7 +238,7 @@ function CredentialForm({
size="sm"
disabled={isSaving || !accountKey.trim() || (showBaseUrl && !baseUrl.trim())}
onClick={() =>
onSave({ account_key: accountKey.trim(), display_name: displayName.trim(), api_key: apiKey, base_url: baseUrl.trim() })
onSave({ account_key: accountKey.trim(), display_name: displayName.trim(), api_key: apiKey, session_key: sessionKey, base_url: baseUrl.trim() })
}
>
{isSaving ? (
@ -267,7 +299,7 @@ function fmtTokens(n: number | null | undefined): string {
}
function fmtResetMs(ms: number | null | undefined): string {
if (ms == null || ms <= 0) return "now";
if (ms == null || ms <= 0) return "< 1m";
const s = Math.floor(ms / 1000);
if (s < 60) return `${s}s`;
const m = Math.floor(s / 60);
@ -366,7 +398,9 @@ function UsageStrip({ credentialId, provider }: { credentialId: string; provider
const tok = usage.tokens;
const inputTok = usage.input_tokens;
const outputTok = usage.output_tokens;
const req = usage.requests;
const subWindows = usage.subscription_windows ?? [];
const isOllama = provider === "ollama";
const sourceLabel: Record<string, string> = {
provider_native: "Provider native",
@ -374,7 +408,16 @@ function UsageStrip({ credentialId, provider }: { credentialId: string; provider
local_jsonl_estimate: "Local estimate",
configured_limit: "Configured limit",
};
// output_tokens is the tightest Anthropic limit (90k vs 450k input, 540k combined)
// — show it first so the binding constraint is prominent
const usageBars: UsageWindowBarProps[] = [];
if (outputTok?.pct_used != null) {
usageBars.push({
label: "API rate limit · output tokens",
pct: outputTok.pct_used,
resetInMs: outputTok.reset_in_ms,
});
}
if (inputTok.pct_used != null) {
usageBars.push({
label: "API rate limit · input tokens",
@ -384,7 +427,7 @@ function UsageStrip({ credentialId, provider }: { credentialId: string; provider
}
if (tok.pct_used != null) {
usageBars.push({
label: "API rate limit · tokens",
label: "API rate limit · tokens (combined)",
pct: tok.pct_used,
resetInMs: tok.reset_in_ms,
});
@ -399,11 +442,31 @@ function UsageStrip({ credentialId, provider }: { credentialId: string; provider
return (
<div className="mt-2 rounded-lg border border-[color:var(--border)] bg-[color:var(--surface)] p-2.5">
{usage.error && (
<div className="mb-2 rounded border border-[color:rgba(248,113,113,0.35)] bg-[color:rgba(248,113,113,0.08)] px-2 py-1.5 text-xs text-[color:var(--danger)]">
{usage.error}
</div>
)}
{/* ── Subscription usage windows (from session key) ── */}
{subWindows.length > 0 && (
<div className="mb-2 space-y-2">
{usage.subscription_plan && (
<div className="text-[11px] text-muted">Plan: {usage.subscription_plan}</div>
)}
{subWindows.map((w) => (
<UsageWindowBar
key={w.key}
label={w.label}
pct={w.pct_used}
resetInMs={w.reset_in_ms}
/>
))}
</div>
)}
{isOllama ? (
<div className="space-y-1.5">
<div className="text-[11px] text-muted">
Source: {sourceLabel[usage.source] ?? usage.source} · confidence: {usage.confidence}
</div>
<div className="flex items-center gap-3 text-xs text-muted">
<span className="flex items-center gap-1 text-[color:var(--success)]">
<span className="inline-block h-1.5 w-1.5 rounded-full bg-[color:var(--success)]" />
@ -416,34 +479,19 @@ function UsageStrip({ credentialId, provider }: { credentialId: string; provider
<RefreshCw className={`h-3 w-3 ${loading ? "animate-spin" : ""}`} />
</button>
</div>
{usageBars.length > 0 ? (
<div className="space-y-2">
{usageBars.map((bar) => (
<UsageWindowBar key={bar.label} label={bar.label} pct={bar.pct} resetInMs={bar.resetInMs} />
))}
{(usage.sample_input_tokens != null || usage.sample_output_tokens != null) && (
<div className="flex items-center justify-between text-[11px] text-muted">
<span>Usage (last probe)</span>
<span className="tabular-nums text-strong">
in {fmtTokens(usage.sample_input_tokens)} · out {fmtTokens(usage.sample_output_tokens)}
</span>
</div>
)}
{usage.sample_latency_ms != null && (
<div className="flex items-center justify-between text-[11px] text-muted">
<span>Time (last probe)</span>
<span className="tabular-nums text-strong">{fmtLatencyMs(usage.sample_latency_ms)}</span>
</div>
) : (
<>
{(usage.sample_input_tokens != null || usage.sample_output_tokens != null) && (
<div className="flex items-center justify-between text-[11px] text-muted">
<span>Usage (last probe)</span>
<span className="tabular-nums text-strong">
in {fmtTokens(usage.sample_input_tokens)} · out {fmtTokens(usage.sample_output_tokens)}
</span>
</div>
)}
{usage.sample_latency_ms != null && (
<div className="flex items-center justify-between text-[11px] text-muted">
<span>Time (last probe)</span>
<span className="tabular-nums text-strong">
{fmtLatencyMs(usage.sample_latency_ms)}
</span>
</div>
)}
<p className="text-[11px] text-muted">
Provider did not return API rate-limit windows for percent + reset diagnostics.
</p>
</>
)}
<div className="flex items-center justify-between text-[11px] text-muted">
{lastFetched && <span>Updated {Math.round((Date.now() - lastFetched.getTime()) / 1000)}s ago</span>}
@ -451,16 +499,19 @@ function UsageStrip({ credentialId, provider }: { credentialId: string; provider
</div>
) : (
<div className="space-y-1.5">
<div className="text-[11px] text-muted">
Source: {sourceLabel[usage.source] ?? usage.source} · confidence: {usage.confidence}
</div>
{/* API rate-limit diagnostic bars */}
{usageBars.length > 0 ? (
<div className="space-y-2">
{usageBars.map((bar) => (
<UsageWindowBar key={bar.label} label={bar.label} pct={bar.pct} resetInMs={bar.resetInMs} />
))}
</div>
) : (
<>
<div className="text-[11px] text-muted">
{sourceLabel[usage.source] ?? usage.source} · {usage.confidence} confidence
</div>
<div className="space-y-2">
{usageBars.map((bar) => (
<UsageWindowBar key={bar.label} label={bar.label} pct={bar.pct} resetInMs={bar.resetInMs} />
))}
</div>
</>
) : subWindows.length === 0 ? (
<>
{(usage.sample_input_tokens != null || usage.sample_output_tokens != null) && (
<div className="flex items-center justify-between text-[11px] text-muted">
@ -482,7 +533,7 @@ function UsageStrip({ credentialId, provider }: { credentialId: string; provider
Connected provider did not return API rate-limit windows for percent + reset diagnostics.
</p>
</>
)}
) : null}
<div className="flex items-center justify-between text-[11px] text-muted">
{lastFetched && <span>Updated {Math.round((Date.now() - lastFetched.getTime()) / 1000)}s ago</span>}
@ -539,6 +590,12 @@ function CredentialRow({ cred, isAdmin, onDelete, onToggle, showUsage = true }:
) : (
<span className="text-[color:var(--warning)]">no key</span>
)}
{cred.has_session_key && (
<span className="flex items-center gap-1 text-[color:var(--success)]">
<KeyRound className="h-3 w-3" />
subscription key {cred.session_key_last_four ?? ""}
</span>
)}
{cred.base_url && <span className="truncate max-w-[200px]">{cred.base_url}</span>}
</div>
</div>
@ -586,7 +643,7 @@ interface ProviderSectionProps {
provider: (typeof PROVIDERS)[number];
credentials: ProviderCredentialRead[];
isAdmin: boolean;
onAdd: (providerId: ProviderId, data: { account_key: string; display_name: string; api_key: string; base_url: string }) => Promise<void>;
onAdd: (providerId: ProviderId, data: { account_key: string; display_name: string; api_key: string; session_key: string; base_url: string }) => Promise<void>;
onTest: (providerId: ProviderId, data: { account_key: string; api_key: string; base_url: string }) => Promise<ProviderUsageLiveRead>;
onDelete: (cred: ProviderCredentialRead) => void;
onToggle: (cred: ProviderCredentialRead) => Promise<void>;
@ -629,6 +686,9 @@ function ProviderSection({ provider, credentials, isAdmin, onAdd, onTest, onDele
providerId={provider.id}
allowMultiple={provider.allowMultiple}
showBaseUrl={provider.showBaseUrl}
showSessionKey={provider.showSessionKey}
sessionKeyLabel={provider.sessionKeyLabel}
sessionKeyPlaceholder={provider.sessionKeyPlaceholder}
keyLabel={provider.keyLabel}
keyPlaceholder={provider.keyPlaceholder}
accountKeyDefault={provider.accountKeyDefault}
@ -708,13 +768,14 @@ export default function AIProvidersSettingsPage() {
const handleAdd = async (
providerId: ProviderId,
data: { account_key: string; display_name: string; api_key: string; base_url: string },
data: { account_key: string; display_name: string; api_key: string; session_key: string; base_url: string },
) => {
const res = await createProviderCredentialApiV1ProviderCredentialsPost({
provider: providerId,
account_key: data.account_key,
display_name: data.display_name || data.account_key,
api_key: data.api_key || undefined,
session_key: data.session_key || undefined,
base_url: data.base_url || undefined,
});
if (res.status === 201) {