diff --git a/open-sse/config/providerModels.js b/open-sse/config/providerModels.js index 1cc4ee5f..42b22f0e 100644 --- a/open-sse/config/providerModels.js +++ b/open-sse/config/providerModels.js @@ -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 diff --git a/open-sse/executors/codex.js b/open-sse/executors/codex.js index ca062ecb..0093193a 100644 --- a/open-sse/executors/codex.js +++ b/open-sse/executors/codex.js @@ -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"; @@ -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 */ diff --git a/open-sse/services/usage.js b/open-sse/services/usage.js index bf0c28ab..646f0a76 100644 --- a/open-sse/services/usage.js +++ b/open-sse/services/usage.js @@ -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}`, @@ -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: {} diff --git a/open-sse/utils/usageTracking.js b/open-sse/utils/usageTracking.js index 810f6235..467c2fa7 100644 --- a/open-sse/utils/usageTracking.js +++ b/open-sse/utils/usageTracking.js @@ -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 diff --git a/src/app/(dashboard)/dashboard/profile/page.js b/src/app/(dashboard)/dashboard/profile/page.js index 63d985b2..38c1dec6 100644 --- a/src/app/(dashboard)/dashboard/profile/page.js +++ b/src/app/(dashboard)/dashboard/profile/page.js @@ -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 ( @@ -597,6 +612,34 @@ export default function ProfilePage() { + {/* Provider Configuration */} + +
+
+ settings +
+

Provider Configuration

+
+
+
+ + updateKiroAuthUrl(e.target.value)} + placeholder="https://view.awsapps.com/start" + disabled={loading} + className="font-mono text-sm" + /> +

+ Custom AWS SSO start URL for Kiro authentication. Leave empty to use default. +

+
+
+
+ {/* Observability Settings */}
diff --git a/src/app/(dashboard)/dashboard/providers/[id]/page.js b/src/app/(dashboard)/dashboard/providers/[id]/page.js index 11869b20..dcc6ff34 100644 --- a/src/app/(dashboard)/dashboard/providers/[id]/page.js +++ b/src/app/(dashboard)/dashboard/providers/[id]/page.js @@ -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) @@ -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 @@ -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 (
@@ -1500,7 +1580,7 @@ function ConnectionRow({ connection, proxyPools, isOAuth, isFirst, isLast, onMov

{displayName}

-
+
{connection.isActive === false ? "disabled" : (effectiveStatus || "Unknown")} @@ -1510,11 +1590,21 @@ function ConnectionRow({ connection, proxyPools, isOAuth, isFirst, isLast, onMov )} {isCooldown && connection.isActive !== false && } + {tokenExpiry && ( + + {tokenExpiry.text} + + )} {connection.lastError && connection.isActive !== false && ( {connection.lastError} )} + {refreshMessage && ( + + {refreshMessage.text} + + )} #{connection.priority} {connection.globalPriority && ( Auto: {connection.globalPriority} @@ -1575,6 +1665,19 @@ function ConnectionRow({ connection, proxyPools, isOAuth, isFirst, isLast, onMov )}
)} + {isOAuth && ( + + )}