Skip to content
Draft
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
12 changes: 10 additions & 2 deletions open-sse/config/providerModels.js
Original file line number Diff line number Diff line change
Expand Up @@ -104,11 +104,19 @@ export const PROVIDER_MODELS = {
{ id: "oswe-vscode-prime", name: "Raptor Mini" },
],
kr: [ // Kiro AI
// { id: "claude-opus-4.5", name: "Claude Opus 4.5" },
// Claude 4.6 series
{ id: "claude-opus-4.6", name: "Claude Opus 4.6" },
{ id: "claude-sonnet-4.6", name: "Claude Sonnet 4.6" },
// Claude 4.5 series
{ id: "claude-opus-4.5", name: "Claude Opus 4.5" },
{ id: "claude-sonnet-4.5", name: "Claude Sonnet 4.5" },
{ id: "claude-haiku-4.5", name: "Claude Haiku 4.5" },
{ id: "deepseek-3.2", name: "DeepSeek 3.2" },
// Claude 4 series
{ id: "claude-sonnet-4", name: "Claude Sonnet 4" },
// Other models
{ id: "deepseek-3.2", name: "DeepSeek V3.2" },
{ id: "deepseek-3.1", name: "DeepSeek 3.1" },
{ id: "minimax-m2.1", name: "MiniMax M2.1" },
{ id: "qwen3-coder-next", name: "Qwen3 Coder Next" },
],
cu: [ // Cursor IDE
Expand Down
32 changes: 32 additions & 0 deletions open-sse/executors/codex.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { BaseExecutor } from "./base.js";
import { CODEX_DEFAULT_INSTRUCTIONS } from "../config/codexInstructions.js";
import { OAUTH_ENDPOINTS } from "../config/constants.js";
import { PROVIDERS } from "../config/providers.js";
import { normalizeResponsesInput } from "../translator/helpers/responsesApiHelper.js";

Expand All @@ -12,6 +13,37 @@ export class CodexExecutor extends BaseExecutor {
super("codex", PROVIDERS.codex);
}

async refreshCredentials(credentials, log) {
if (!credentials.refreshToken) return null;

try {
const response = await fetch(OAUTH_ENDPOINTS.openai.token, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded", "Accept": "application/json" },
body: new URLSearchParams({
grant_type: "refresh_token",
refresh_token: credentials.refreshToken,
client_id: this.config.clientId,
scope: "openid profile email offline_access"
})
});

if (!response.ok) return null;

const tokens = await response.json();
log?.info?.("TOKEN", "Codex refreshed");

return {
accessToken: tokens.access_token,
refreshToken: tokens.refresh_token || credentials.refreshToken,
expiresIn: tokens.expires_in
};
} catch (error) {
log?.error?.("TOKEN", `Codex refresh error: ${error.message}`);
return null;
}
}

/**
* Override headers to add session_id per request
*/
Expand Down
36 changes: 29 additions & 7 deletions open-sse/services/usage.js
Original file line number Diff line number Diff line change
Expand Up @@ -533,19 +533,26 @@ async function getCodexUsage(accessToken) {
* Kiro (AWS CodeWhisperer) Usage
*/
async function getKiroUsage(accessToken, providerSpecificData) {
// Default profileArn fallback
const DEFAULT_PROFILE_ARN = "arn:aws:codewhisperer:us-east-1:638616132270:profile/AAAACCCCXXXX";
const profileArn = providerSpecificData?.profileArn || DEFAULT_PROFILE_ARN;
// profileArn is optional and only needed for specific AWS account filtering
const profileArn = providerSpecificData?.profileArn;
const region = providerSpecificData?.region || "us-east-1";

try {
// Try old API first (POST method)
const payload = {
origin: "AI_EDITOR",
profileArn: profileArn,
resourceType: "AGENTIC_REQUEST",
};

const response = await fetch("https://codewhisperer.us-east-1.amazonaws.com", {
// Only add profileArn if available
if (profileArn) {
payload.profileArn = profileArn;
}

// Use region-specific endpoint
const endpoint = `https://codewhisperer.${region}.amazonaws.com`;

const response = await fetch(endpoint, {
method: "POST",
headers: {
"Authorization": `Bearer ${accessToken}`,
Expand All @@ -559,8 +566,23 @@ async function getKiroUsage(accessToken, providerSpecificData) {
if (!response.ok) {
const errorText = await response.text();

// Handle authentication errors gracefully
if (response.status === 403 || response.status === 401) {
// Handle specific error cases with user-friendly messages
if (response.status === 403) {
try {
const errorData = JSON.parse(errorText);
if (errorData.reason === "FEATURE_NOT_SUPPORTED") {
return {
message: "Kiro connected. Usage tracking unavailable for this AWS organization."
};
}
} catch (e) {
// Continue with generic error
}
return {
message: "Kiro connected. Unable to fetch usage data."
};
}
if (response.status === 401) {
return {
message: "Kiro quota API authentication expired. Chat may still work.",
quotas: {}
Expand Down
5 changes: 5 additions & 0 deletions open-sse/utils/usageTracking.js
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,11 @@ export function logUsage(provider, usage, model = null, connectionId = null, api
const reasoning = usage.reasoning_tokens;
if (reasoning) msg += ` | reasoning=${reasoning}`;

// Note for Kiro: credit-based pricing
if (provider === "kiro") {
msg += ` ${COLORS.yellow}(credit-based - see dashboard for actual usage)${COLORS.reset}`;
}

console.log(msg);

// Save to usage DB
Expand Down
43 changes: 43 additions & 0 deletions src/app/(dashboard)/dashboard/profile/page.js
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,21 @@ export default function ProfilePage() {
}
};

const updateKiroAuthUrl = async (url) => {
try {
const res = await fetch("/api/settings", {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ kiroAuthUrl: url.trim() || "https://view.awsapps.com/start" }),
});
if (res.ok) {
setSettings(prev => ({ ...prev, kiroAuthUrl: url.trim() || "https://view.awsapps.com/start" }));
}
} catch (err) {
console.error("Failed to update Kiro auth URL:", err);
}
};

const observabilityEnabled = settings.observabilityEnabled === true;

return (
Expand Down Expand Up @@ -597,6 +612,34 @@ export default function ProfilePage() {
</div>
</Card>

{/* Provider Configuration */}
<Card>
<div className="flex items-center gap-3 mb-4">
<div className="p-2 rounded-lg bg-purple-500/10 text-purple-500">
<span className="material-symbols-outlined text-[20px]">settings</span>
</div>
<h3 className="text-lg font-semibold">Provider Configuration</h3>
</div>
<div className="flex flex-col gap-4">
<div>
<label className="block text-sm font-medium mb-2">
Kiro AWS Builder ID Start URL <span className="text-xs text-text-muted">(Optional)</span>
</label>
<Input
type="text"
value={settings.kiroAuthUrl || ""}
onChange={(e) => updateKiroAuthUrl(e.target.value)}
placeholder="https://view.awsapps.com/start"
disabled={loading}
className="font-mono text-sm"
/>
<p className="text-xs text-text-muted mt-2">
Custom AWS SSO start URL for Kiro authentication. Leave empty to use default.
</p>
</div>
</div>
</Card>

{/* Observability Settings */}
<Card>
<div className="flex items-center gap-3 mb-4">
Expand Down
107 changes: 106 additions & 1 deletion src/app/(dashboard)/dashboard/providers/[id]/page.js
Original file line number Diff line number Diff line change
Expand Up @@ -1438,6 +1438,9 @@ function ConnectionRow({ connection, proxyPools, isOAuth, isFirst, isLast, onMov

// Use useState + useEffect for impure Date.now() to avoid calling during render
const [isCooldown, setIsCooldown] = useState(false);
const [refreshing, setRefreshing] = useState(false);
const [refreshMessage, setRefreshMessage] = useState(null);
const [tokenExpiry, setTokenExpiry] = useState(null);

// Get earliest model lock timestamp (useEffect handles the Date.now() comparison)
const modelLockUntil = Object.entries(connection)
Expand All @@ -1463,6 +1466,48 @@ function ConnectionRow({ connection, proxyPools, isOAuth, isFirst, isLast, onMov
};
}, [modelLockUntil]);

// Calculate token expiry info
useEffect(() => {
if (!isOAuth || !connection.tokenExpiresAt) {
setTokenExpiry(null);
return;
}

const updateExpiry = () => {
const expiresAt = new Date(connection.tokenExpiresAt).getTime();
const now = Date.now();
const diff = expiresAt - now;

if (diff <= 0) {
setTokenExpiry({ expired: true, text: "Expired", warning: true });
} else {
const minutes = Math.floor(diff / 60000);
const hours = Math.floor(minutes / 60);
const days = Math.floor(hours / 24);

let text;
if (days > 0) {
text = `${days}d`;
} else if (hours > 0) {
text = `${hours}h`;
} else {
text = `${minutes}m`;
}

setTokenExpiry({
expired: false,
text: `Expires in ${text}`,
warning: minutes < 5,
minutes,
});
}
};

updateExpiry();
const interval = setInterval(updateExpiry, 60000); // Update every minute
return () => clearInterval(interval);
}, [connection.tokenExpiresAt, isOAuth]);

// Determine effective status (override unavailable if cooldown expired)
const effectiveStatus = (connection.testStatus === "unavailable" && !isCooldown)
? "active" // Cooldown expired → treat as active
Expand All @@ -1475,6 +1520,41 @@ function ConnectionRow({ connection, proxyPools, isOAuth, isFirst, isLast, onMov
return "default";
};

const handleRefreshToken = async () => {
setRefreshing(true);
setRefreshMessage(null);

try {
const res = await fetch("/api/oauth/refresh", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ connectionId: connection.id }),
});

const data = await res.json();

if (res.ok) {
setRefreshMessage({ type: "success", text: "Token refreshed successfully" });
// Update local token expiry
if (data.expiresAt) {
connection.tokenExpiresAt = data.expiresAt;
}
// Auto-hide success message after 3 seconds
setTimeout(() => setRefreshMessage(null), 3000);
} else {
const errorMsg = data.error || "Failed to refresh token";
setRefreshMessage({ type: "error", text: errorMsg });
// Auto-hide error message after 5 seconds
setTimeout(() => setRefreshMessage(null), 5000);
}
} catch (error) {
setRefreshMessage({ type: "error", text: "Network error. Please try again." });
setTimeout(() => setRefreshMessage(null), 5000);
} finally {
setRefreshing(false);
}
};

return (
<div className={`group flex items-center justify-between p-3 rounded-lg hover:bg-black/[0.02] dark:hover:bg-white/[0.02] transition-colors ${connection.isActive === false ? "opacity-60" : ""}`}>
<div className="flex items-center gap-3 flex-1 min-w-0">
Expand All @@ -1500,7 +1580,7 @@ function ConnectionRow({ connection, proxyPools, isOAuth, isFirst, isLast, onMov
</span>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium truncate">{displayName}</p>
<div className="flex items-center gap-2 mt-1">
<div className="flex items-center gap-2 mt-1 flex-wrap">
<Badge variant={getStatusVariant()} size="sm" dot>
{connection.isActive === false ? "disabled" : (effectiveStatus || "Unknown")}
</Badge>
Expand All @@ -1510,11 +1590,21 @@ function ConnectionRow({ connection, proxyPools, isOAuth, isFirst, isLast, onMov
</Badge>
)}
{isCooldown && connection.isActive !== false && <CooldownTimer until={modelLockUntil} />}
{tokenExpiry && (
<span className={`text-xs font-mono ${tokenExpiry.warning ? "text-orange-500" : "text-text-muted"}`} title={connection.tokenExpiresAt}>
{tokenExpiry.text}
</span>
)}
{connection.lastError && connection.isActive !== false && (
<span className="text-xs text-red-500 truncate max-w-[300px]" title={connection.lastError}>
{connection.lastError}
</span>
)}
{refreshMessage && (
<span className={`text-xs ${refreshMessage.type === "success" ? "text-green-500" : "text-red-500"}`}>
{refreshMessage.text}
</span>
)}
<span className="text-xs text-text-muted">#{connection.priority}</span>
{connection.globalPriority && (
<span className="text-xs text-text-muted">Auto: {connection.globalPriority}</span>
Expand Down Expand Up @@ -1575,6 +1665,19 @@ function ConnectionRow({ connection, proxyPools, isOAuth, isFirst, isLast, onMov
)}
</div>
)}
{isOAuth && (
<button
onClick={handleRefreshToken}
disabled={refreshing}
className="flex flex-col items-center px-2 py-1 rounded hover:bg-black/5 dark:hover:bg-white/5 text-text-muted hover:text-primary disabled:opacity-30 disabled:cursor-not-allowed"
title="Manually refresh OAuth token"
>
<span className={`material-symbols-outlined text-[18px] ${refreshing ? "animate-spin" : ""}`}>
{refreshing ? "progress_activity" : "refresh"}
</span>
<span className="text-[10px] leading-tight">Refresh</span>
</button>
)}
<button onClick={onEdit} className="flex flex-col items-center px-2 py-1 rounded hover:bg-black/5 dark:hover:bg-white/5 text-text-muted hover:text-primary">
<span className="material-symbols-outlined text-[18px]">edit</span>
<span className="text-[10px] leading-tight">Edit</span>
Expand Down Expand Up @@ -1607,6 +1710,8 @@ ConnectionRow.propTypes = {
lastError: PropTypes.string,
priority: PropTypes.number,
globalPriority: PropTypes.number,
tokenExpiresAt: PropTypes.string,
authType: PropTypes.string,
}).isRequired,
proxyPools: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.string,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,9 @@ export default function QuotaTable({ quotas = [] }) {
<div className="overflow-x-auto">
<table className="w-full table-fixed">
<colgroup>
<col className="w-[30%]" /> {/* Model Name */}
<col className="w-[45%]" /> {/* Limit Progress */}
<col className="w-[25%]" /> {/* Reset Time */}
<col className="w-[30%]" />
<col className="w-[45%]" />
<col className="w-[25%]" />
</colgroup>
<tbody>
{quotas.map((quota, index) => {
Expand Down
Loading