mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-09-30 13:53:15 +00:00
* feat: dashboard backend — GitHub OAuth, profile CRUD, admin endpoints Adds agent/dashboard/ FastAPI router mounted at /dashboard/api covering: - GitHub App OAuth login → JWT cookie session (cross-domain ready) - profile CRUD against LangGraph Store with model+effort validation - admin gate via CONFIGURED_ADMINS - /repos via /user/installations using the user's encrypted OAuth token CORS allowlist on webapp.py is opt-in via DASHBOARD_ALLOWED_ORIGINS so the Vercel-hosted frontend can call the LangSmith deployment with credentials. * feat: apply dashboard profile model/effort overrides in get_agent Look up the triggering user's GitHub login from config (direct field or GITHUB_USER_EMAIL_MAP reverse lookup), read their profile from the Store, and apply default_model + reasoning_effort to make_model when both are valid. Effort 'max' is captured on the profile but not yet wired through — the OpenAI Reasoning Literal doesn't accept it. * feat: ui/ TanStack Start dashboard for profile config Scaffolded with the shadcn b7CScJIjA preset (TanStack Start template, base-ui primitives, Tailwind v4). Three routes: - /login — Sign in with GitHub (links to /dashboard/api/auth/login) - /profile — Edit default model, reasoning effort, default repo - /admin — Admin-only: list users and edit other profiles API client (src/lib/api.ts) uses credentials: include so the osw_session cookie set by the OAuth callback rides cross-origin. VITE_DASHBOARD_API_BASE_URL points at the LangSmith deployment. Effort options re-render when the model changes; 'max' on Opus 4.7 is captured on the profile but ignored downstream until anthropic reasoning is wired through make_model. * feat: searchable Combobox for default repo picker Replaces the Select with a base-ui Combobox so users can filter by typing, the popup is wider than the trigger so full owner/repo names are readable, and the list caps at max-h-80 to stay on screen. * fix: address review comments + wire default_repo and Anthropic thinking Security/correctness fixes from PR review: * Open redirect: validate `redirect_to` in `/auth/login` against `DASHBOARD_BASE_URL` + `DASHBOARD_ALLOWED_ORIGINS` before signing it into the state JWT. Anything off-allowlist falls back to the dashboard base URL. (PR #1302 r3250054386) * Login CSRF: bind the OAuth `state` to the requesting browser. At `/auth/login` we generate a fresh nonce, set it as a short-lived HttpOnly SameSite=Lax cookie scoped to `/dashboard/api/auth`, and embed `hash_state_nonce(nonce)` in the state JWT. At `/auth/callback` we require the cookie nonce to hash-match the state JWT's nonce_hash (constant-time compare). (PR #1302 r3250054395) * RMW race in profile vs token writes: split storage into two namespaces — `["profiles"]` for user-editable settings and `["oauth_tokens"]` for the encrypted GitHub token. Each upsert now only writes its own namespace so an in-flight profile save can no longer clobber a fresh token from a concurrent re-login (and vice versa). (PR #1302 r3250054393) * /repos pagination: follow `Link: rel="next"` for both `/user/installations` and per-installation `/repositories` with per_page=100, capped at 1000 items. (PR #1302 r3250054401) Feature wires: * default_repo: applied as a fallback in `get_slack_repo_config` (after explicit-repo / thread metadata, before the env defaults) and in the Linear webhook (after comment-body extraction, before team mapping). Both paths resolve the triggering user's GitHub login via GITHUB_USER_EMAIL_MAP and read the profile's default_repo. * Anthropic "thinking" effort: `make_model` now accepts a `thinking` kwarg; `get_agent` maps profile effort {low,medium,high,xhigh,max} to budget_tokens {1k,4k,12k,32k,60k} when the chosen model is anthropic. OpenAI path still ignores "max" since the Literal doesn't accept it.
55 lines
1.8 KiB
Python
55 lines
1.8 KiB
Python
from typing import Literal, TypedDict, Unpack
|
|
|
|
from langchain.chat_models import init_chat_model
|
|
|
|
OPENAI_RESPONSES_WS_BASE_URL = "wss://api.openai.com/v1"
|
|
|
|
# Anthropic SDK default is 2; a 529 burst can outlive that. Bump to give the
|
|
# primary provider a fair chance before the fallback middleware kicks in.
|
|
DEFAULT_MAX_RETRIES = 6
|
|
|
|
|
|
OpenAIReasoningEffort = Literal["none", "low", "medium", "high", "xhigh"]
|
|
AnthropicThinkingType = Literal["enabled", "disabled"]
|
|
|
|
|
|
class OpenAIReasoning(TypedDict, total=False):
|
|
effort: OpenAIReasoningEffort
|
|
|
|
|
|
class AnthropicThinking(TypedDict, total=False):
|
|
type: AnthropicThinkingType
|
|
budget_tokens: int
|
|
|
|
|
|
class ModelKwargs(TypedDict, total=False):
|
|
max_tokens: int | None
|
|
reasoning: OpenAIReasoning | None
|
|
thinking: AnthropicThinking | None
|
|
temperature: float | None
|
|
max_retries: int | None
|
|
|
|
|
|
def make_model(model_id: str, **kwargs: Unpack[ModelKwargs]):
|
|
model_kwargs: dict[str, object] = kwargs.copy()
|
|
model_kwargs.setdefault("max_retries", DEFAULT_MAX_RETRIES)
|
|
|
|
if model_id.startswith("openai:"):
|
|
model_kwargs["base_url"] = OPENAI_RESPONSES_WS_BASE_URL
|
|
model_kwargs["use_responses_api"] = True
|
|
|
|
return init_chat_model(model=model_id, **model_kwargs)
|
|
|
|
|
|
def fallback_model_id_for(primary_model_id: str) -> str | None:
|
|
"""Return the cross-provider fallback model id for a given primary, if any.
|
|
|
|
Anthropic primaries fall back to OpenAI and vice versa. Returns ``None``
|
|
when the provider has no configured cross-provider fallback (e.g. local
|
|
or self-hosted providers we don't want to silently route off-host).
|
|
"""
|
|
if primary_model_id.startswith("anthropic:"):
|
|
return "openai:gpt-5.5"
|
|
if primary_model_id.startswith("openai:"):
|
|
return "anthropic:claude-opus-4-5"
|
|
return None
|