mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-13 05:07:40 +02:00
[eric] fix subscription provider IDs, handle both OAuth flows
- Fixed provider IDs to match 9Router: claude, codex, github, gemini-cli
- Handle authorization_code_pkce flow (Claude, Codex) — opens browser, polls status
- Handle device_code flow (GitHub, Qwen, Kiro) — shows code, polls for completion
- Pass code_verifier and extra_data through poll endpoint
- Fix isConnected check for 9Router's {connections: [...]} response format
- Auto-start 9Router in backend lifespan
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
50b28f7080
commit
0a907e4bac
@@ -326,7 +326,11 @@ async def subscriptions_poll(body: dict):
|
||||
raise HTTPException(status_code=400, detail="provider and device_code required")
|
||||
|
||||
try:
|
||||
result = await poll_oauth(provider, device_code)
|
||||
result = await poll_oauth(
|
||||
provider, device_code,
|
||||
code_verifier=body.get("code_verifier"),
|
||||
extra_data=body.get("extra_data"),
|
||||
)
|
||||
return result
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@@ -101,25 +101,60 @@ async def get_providers() -> list[dict]:
|
||||
|
||||
|
||||
async def start_oauth(provider: str) -> dict:
|
||||
"""Start OAuth device flow for a provider.
|
||||
"""Start OAuth flow for a provider.
|
||||
|
||||
Returns: {user_code, verification_uri, device_code, ...}
|
||||
For device_code providers (github, qwen, kiro): returns {user_code, verification_uri, device_code}
|
||||
For authorization_code providers (claude, codex, gemini-cli): returns {authUrl, codeVerifier, state}
|
||||
"""
|
||||
async with httpx.AsyncClient(timeout=15.0) as client:
|
||||
r = await client.get(f"{NINE_ROUTER_API}/oauth/{provider}/device-code")
|
||||
# Try device-code flow first
|
||||
try:
|
||||
r = await client.get(f"{NINE_ROUTER_API}/oauth/{provider}/device-code")
|
||||
if r.status_code == 200:
|
||||
data = r.json()
|
||||
return {
|
||||
"flow": "device_code",
|
||||
"user_code": data.get("user_code", ""),
|
||||
"verification_uri": data.get("verification_uri", data.get("verification_uri_complete", "")),
|
||||
"device_code": data.get("device_code", ""),
|
||||
"code_verifier": data.get("codeVerifier", ""),
|
||||
"extra_data": {k: v for k, v in data.items() if k.startswith("_")},
|
||||
}
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Fall back to authorization_code flow
|
||||
callback_url = f"http://localhost:{NINE_ROUTER_PORT}/api/oauth/{provider}/callback"
|
||||
r = await client.get(
|
||||
f"{NINE_ROUTER_API}/oauth/{provider}/authorize",
|
||||
params={"redirect_uri": callback_url},
|
||||
)
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
data = r.json()
|
||||
return {
|
||||
"flow": "authorization_code",
|
||||
"auth_url": data.get("authUrl", ""),
|
||||
"code_verifier": data.get("codeVerifier", ""),
|
||||
"state": data.get("state", ""),
|
||||
"redirect_uri": callback_url,
|
||||
}
|
||||
|
||||
|
||||
async def poll_oauth(provider: str, device_code: str) -> dict:
|
||||
async def poll_oauth(provider: str, device_code: str, code_verifier: str | None = None, extra_data: dict | None = None) -> dict:
|
||||
"""Poll for OAuth completion.
|
||||
|
||||
Returns: {status: "pending"} or {status: "connected", connection: {...}}
|
||||
Returns: {success: true, connection: {...}} or {success: false, pending: true}
|
||||
"""
|
||||
body: dict = {"deviceCode": device_code}
|
||||
if code_verifier:
|
||||
body["codeVerifier"] = code_verifier
|
||||
if extra_data:
|
||||
body["extraData"] = extra_data
|
||||
|
||||
async with httpx.AsyncClient(timeout=15.0) as client:
|
||||
r = await client.post(
|
||||
f"{NINE_ROUTER_API}/oauth/{provider}/poll",
|
||||
json={"deviceCode": device_code},
|
||||
json=body,
|
||||
)
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
|
||||
@@ -158,10 +158,10 @@ const CopilotAuthButton: React.FC = () => {
|
||||
|
||||
// ── Subscription Provider Card ──
|
||||
const SUBSCRIPTION_PROVIDERS = [
|
||||
{ id: 'claude-code', name: 'Claude Pro / Max', desc: 'Sonnet, Opus, Haiku — use your Anthropic subscription', color: '#E8927A' },
|
||||
{ id: 'openai-codex', name: 'ChatGPT Plus / Pro', desc: 'GPT-5.4, o3, o4-mini — use your OpenAI subscription', color: '#74AA9C' },
|
||||
{ id: 'claude', name: 'Claude Pro / Max', desc: 'Sonnet, Opus, Haiku — use your Anthropic subscription', color: '#E8927A' },
|
||||
{ id: 'codex', name: 'ChatGPT Plus / Pro', desc: 'GPT-5.4, o3, o4-mini — use your OpenAI subscription', color: '#74AA9C' },
|
||||
{ id: 'github', name: 'GitHub Copilot', desc: 'Claude + GPT models via your Copilot subscription', color: '#8B949E' },
|
||||
{ id: 'gemini', name: 'Gemini Advanced', desc: 'Gemini 2.5 Pro and Flash — use your Google subscription', color: '#4285F4' },
|
||||
{ id: 'gemini-cli', name: 'Gemini Advanced', desc: 'Gemini 2.5 Pro and Flash — use your Google subscription', color: '#4285F4' },
|
||||
];
|
||||
|
||||
const SubscriptionCard: React.FC<{ provider: typeof SUBSCRIPTION_PROVIDERS[0]; connected: boolean; onConnect: () => void; onDisconnect: () => void; connecting: boolean; userCode?: string }> = ({ provider, connected, onConnect, onDisconnect, connecting, userCode }) => {
|
||||
@@ -213,8 +213,8 @@ const SubscriptionCards: React.FC = () => {
|
||||
|
||||
const isConnected = (providerId: string) => {
|
||||
if (!status?.providers) return false;
|
||||
const providers = Array.isArray(status.providers) ? status.providers : [];
|
||||
return providers.some((p: any) => p.provider === providerId && p.isActive);
|
||||
const connections = status.providers?.connections || (Array.isArray(status.providers) ? status.providers : []);
|
||||
return connections.some((p: any) => p.provider === providerId && p.isActive);
|
||||
};
|
||||
|
||||
const handleConnect = async (providerId: string) => {
|
||||
@@ -226,21 +226,21 @@ const SubscriptionCards: React.FC = () => {
|
||||
body: JSON.stringify({ provider: providerId }),
|
||||
});
|
||||
const data = await r.json();
|
||||
if (data.user_code || data.userCode) {
|
||||
const code = data.user_code || data.userCode;
|
||||
|
||||
if (data.flow === 'device_code') {
|
||||
// Device code flow (GitHub, Qwen, etc.) — show code, poll
|
||||
const code = data.user_code || '';
|
||||
setUserCode(code);
|
||||
if (data.verification_uri || data.verificationUri) {
|
||||
window.open(data.verification_uri || data.verificationUri, '_blank');
|
||||
}
|
||||
// Start polling
|
||||
if (data.verification_uri) window.open(data.verification_uri, '_blank');
|
||||
|
||||
const timer = setInterval(async () => {
|
||||
try {
|
||||
const pr = await fetch(`${API_BASE}/agents/subscriptions/poll`, {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ provider: providerId, device_code: data.device_code || data.deviceCode }),
|
||||
body: JSON.stringify({ provider: providerId, device_code: data.device_code, code_verifier: data.code_verifier, extra_data: data.extra_data }),
|
||||
});
|
||||
const pd = await pr.json();
|
||||
if (pd.status === 'connected' || pd.success) {
|
||||
if (pd.success) {
|
||||
clearInterval(timer);
|
||||
setConnecting(null);
|
||||
setUserCode('');
|
||||
@@ -250,6 +250,30 @@ const SubscriptionCards: React.FC = () => {
|
||||
}, 5000);
|
||||
setPollTimer(timer);
|
||||
setTimeout(() => { clearInterval(timer); setConnecting(null); setUserCode(''); }, 300000);
|
||||
|
||||
} else if (data.flow === 'authorization_code') {
|
||||
// Auth code flow (Claude, Codex, Gemini) — open browser, poll for connection
|
||||
if (data.auth_url) window.open(data.auth_url, '_blank');
|
||||
|
||||
// Poll the providers list until this provider appears as connected
|
||||
const timer = setInterval(async () => {
|
||||
try {
|
||||
const sr = await fetch(`${API_BASE}/agents/subscriptions/status`);
|
||||
const sd = await sr.json();
|
||||
const providers = Array.isArray(sd.providers?.connections) ? sd.providers.connections : Array.isArray(sd.providers) ? sd.providers : [];
|
||||
const found = providers.some((p: any) => p.provider === providerId && p.isActive);
|
||||
if (found) {
|
||||
clearInterval(timer);
|
||||
setConnecting(null);
|
||||
fetchStatus();
|
||||
}
|
||||
} catch {}
|
||||
}, 3000);
|
||||
setPollTimer(timer);
|
||||
setTimeout(() => { clearInterval(timer); setConnecting(null); }, 300000);
|
||||
|
||||
} else {
|
||||
setConnecting(null);
|
||||
}
|
||||
} catch { setConnecting(null); }
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user