mirror of
https://github.com/All-Hands-AI/OpenHands.git
synced 2026-04-29 03:00:45 -04:00
Compare commits
11 Commits
sdk/minima
...
self-hoste
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2399174e89 | ||
|
|
7c3f4891f8 | ||
|
|
49bb7bbaba | ||
|
|
10c1252cfe | ||
|
|
911867492c | ||
|
|
85a1b47c8d | ||
|
|
d6011829a3 | ||
|
|
9200e1dbd8 | ||
|
|
d1343539ba | ||
|
|
8bc206833a | ||
|
|
7cf61d8c0e |
@@ -6,38 +6,55 @@ import { KeyStatusIcon } from "../key-status-icon";
|
||||
|
||||
interface GitHubTokenInputProps {
|
||||
onChange: (value: string) => void;
|
||||
onBaseDomainChange?: (value: string) => void;
|
||||
isGitHubTokenSet: boolean;
|
||||
name: string;
|
||||
baseDomainSet?: string | null;
|
||||
isSaas: boolean;
|
||||
}
|
||||
|
||||
export function GitHubTokenInput({
|
||||
onChange,
|
||||
onBaseDomainChange,
|
||||
isGitHubTokenSet,
|
||||
name,
|
||||
baseDomainSet,
|
||||
isSaas,
|
||||
}: GitHubTokenInputProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
{!isSaas && (
|
||||
<SettingsInput
|
||||
testId={name}
|
||||
name={name}
|
||||
onChange={onChange}
|
||||
label={t(I18nKey.GITHUB$TOKEN_LABEL)}
|
||||
type="password"
|
||||
className="w-[680px]"
|
||||
placeholder={isGitHubTokenSet ? "<hidden>" : ""}
|
||||
startContent={
|
||||
isGitHubTokenSet && (
|
||||
<KeyStatusIcon
|
||||
testId="gh-set-token-indicator"
|
||||
isSet={isGitHubTokenSet}
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
<SettingsInput
|
||||
testId={name}
|
||||
name={name}
|
||||
onChange={onChange}
|
||||
label={t(I18nKey.GITHUB$TOKEN_LABEL)}
|
||||
type="password"
|
||||
onChange={onBaseDomainChange || (() => {})}
|
||||
label={t(I18nKey.GITHUB$BASE_DOMAIN_LABEL)}
|
||||
type="text"
|
||||
className="w-[680px]"
|
||||
placeholder={isGitHubTokenSet ? "<hidden>" : ""}
|
||||
startContent={
|
||||
isGitHubTokenSet && (
|
||||
<KeyStatusIcon
|
||||
testId="gh-set-token-indicator"
|
||||
isSet={isGitHubTokenSet}
|
||||
/>
|
||||
)
|
||||
}
|
||||
placeholder={"github.com"}
|
||||
defaultValue={baseDomainSet ? baseDomainSet : undefined}
|
||||
/>
|
||||
|
||||
<GitHubTokenHelpAnchor />
|
||||
{!isSaas && <GitHubTokenHelpAnchor />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,38 +6,55 @@ import { KeyStatusIcon } from "../key-status-icon";
|
||||
|
||||
interface GitLabTokenInputProps {
|
||||
onChange: (value: string) => void;
|
||||
onBaseDomainChange?: (value: string) => void;
|
||||
isGitLabTokenSet: boolean;
|
||||
name: string;
|
||||
baseDomainSet?: string | null;
|
||||
isSaas: boolean;
|
||||
}
|
||||
|
||||
export function GitLabTokenInput({
|
||||
onChange,
|
||||
onBaseDomainChange,
|
||||
isGitLabTokenSet,
|
||||
name,
|
||||
baseDomainSet,
|
||||
isSaas,
|
||||
}: GitLabTokenInputProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
{!isSaas && (
|
||||
<SettingsInput
|
||||
testId={name}
|
||||
name={name}
|
||||
onChange={onChange}
|
||||
label={t(I18nKey.GITLAB$TOKEN_LABEL)}
|
||||
type="password"
|
||||
className="w-[680px]"
|
||||
placeholder={isGitLabTokenSet ? "<hidden>" : ""}
|
||||
startContent={
|
||||
isGitLabTokenSet && (
|
||||
<KeyStatusIcon
|
||||
testId="gl-set-token-indicator"
|
||||
isSet={isGitLabTokenSet}
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
<SettingsInput
|
||||
testId={name}
|
||||
name={name}
|
||||
onChange={onChange}
|
||||
label={t(I18nKey.GITLAB$TOKEN_LABEL)}
|
||||
type="password"
|
||||
onChange={onBaseDomainChange || (() => {})}
|
||||
label={t(I18nKey.GITLAB$BASE_DOMAIN_LABEL)}
|
||||
type="text"
|
||||
className="w-[680px]"
|
||||
placeholder={isGitLabTokenSet ? "<hidden>" : ""}
|
||||
startContent={
|
||||
isGitLabTokenSet && (
|
||||
<KeyStatusIcon
|
||||
testId="gl-set-token-indicator"
|
||||
isSet={isGitLabTokenSet}
|
||||
/>
|
||||
)
|
||||
}
|
||||
placeholder={"gitlab.com"}
|
||||
defaultValue={baseDomainSet ? baseDomainSet : undefined}
|
||||
/>
|
||||
|
||||
<GitLabTokenHelpAnchor />
|
||||
{!isSaas && <GitLabTokenHelpAnchor />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -54,13 +54,11 @@ export const useSettings = () => {
|
||||
React.useEffect(() => {
|
||||
if (query.data?.PROVIDER_TOKENS_SET) {
|
||||
const providers = query.data.PROVIDER_TOKENS_SET;
|
||||
const setProviders = (
|
||||
Object.keys(providers) as Array<keyof typeof providers>
|
||||
).filter((key) => providers[key]);
|
||||
const setProviders = Object.keys(providers) as Array<
|
||||
keyof typeof providers
|
||||
>;
|
||||
setProviderTokensSet(setProviders);
|
||||
const atLeastOneSet = Object.values(query.data.PROVIDER_TOKENS_SET).some(
|
||||
(value) => value,
|
||||
);
|
||||
const atLeastOneSet = setProviders.length > 0;
|
||||
setProvidersAreSet(atLeastOneSet);
|
||||
}
|
||||
}, [query.data?.PROVIDER_TOKENS_SET, query.isFetched]);
|
||||
|
||||
@@ -104,6 +104,7 @@ export enum I18nKey {
|
||||
EXIT_PROJECT$TITLE = "EXIT_PROJECT$TITLE",
|
||||
LANGUAGE$LABEL = "LANGUAGE$LABEL",
|
||||
GITHUB$TOKEN_LABEL = "GITHUB$TOKEN_LABEL",
|
||||
GITHUB$BASE_DOMAIN_LABEL = "GITHUB$BASE_DOMAIN_LABEL",
|
||||
GITHUB$TOKEN_OPTIONAL = "GITHUB$TOKEN_OPTIONAL",
|
||||
GITHUB$GET_TOKEN = "GITHUB$GET_TOKEN",
|
||||
GITHUB$TOKEN_HELP_TEXT = "GITHUB$TOKEN_HELP_TEXT",
|
||||
@@ -450,6 +451,7 @@ export enum I18nKey {
|
||||
MODEL_SELECTOR$VERIFIED = "MODEL_SELECTOR$VERIFIED",
|
||||
MODEL_SELECTOR$OTHERS = "MODEL_SELECTOR$OTHERS",
|
||||
GITLAB$TOKEN_LABEL = "GITLAB$TOKEN_LABEL",
|
||||
GITLAB$BASE_DOMAIN_LABEL = "GITLAB$BASE_DOMAIN_LABEL",
|
||||
GITLAB$GET_TOKEN = "GITLAB$GET_TOKEN",
|
||||
GITLAB$TOKEN_HELP_TEXT = "GITLAB$TOKEN_HELP_TEXT",
|
||||
GITLAB$TOKEN_LINK_TEXT = "GITLAB$TOKEN_LINK_TEXT",
|
||||
|
||||
@@ -1569,6 +1569,21 @@
|
||||
"tr": "GitHub Jetonu",
|
||||
"de": "GitHub-Token"
|
||||
},
|
||||
"GITHUB$BASE_DOMAIN_LABEL": {
|
||||
"en": "GitHub Base Domain",
|
||||
"ja": "GitHub ベースドメイン",
|
||||
"zh-CN": "GitHub 基础域名",
|
||||
"zh-TW": "GitHub 基礎網域",
|
||||
"ko-KR": "GitHub 기본 도메인",
|
||||
"no": "GitHub Base Domain",
|
||||
"it": "Dominio Base GitHub",
|
||||
"pt": "Domínio Base do GitHub",
|
||||
"es": "Dominio Base de GitHub",
|
||||
"ar": "نطاق GitHub الأساسي",
|
||||
"fr": "Domaine de Base GitHub",
|
||||
"tr": "GitHub Temel Alan Adı",
|
||||
"de": "GitHub Basis-Domain"
|
||||
},
|
||||
"GITHUB$TOKEN_OPTIONAL": {
|
||||
"en": "GitHub Token (Optional)",
|
||||
"ja": "GitHubトークン(任意)",
|
||||
@@ -6469,6 +6484,21 @@
|
||||
"tr": "GitLab Jetonu",
|
||||
"de": "GitLab-Token"
|
||||
},
|
||||
"GITLAB$BASE_DOMAIN_LABEL": {
|
||||
"en": "GitLab Base Domain",
|
||||
"ja": "GitLab ベースドメイン",
|
||||
"zh-CN": "GitLab 基础域名",
|
||||
"zh-TW": "GitLab 基礎網域",
|
||||
"ko-KR": "GitLab 기본 도메인",
|
||||
"no": "GitLab Base Domain",
|
||||
"it": "Dominio Base GitLab",
|
||||
"pt": "Domínio Base do GitLab",
|
||||
"es": "Dominio Base de GitLab",
|
||||
"ar": "نطاق GitLab الأساسي",
|
||||
"fr": "Domaine de Base GitLab",
|
||||
"tr": "GitLab Temel Alan Adı",
|
||||
"de": "GitLab Basis-Domain"
|
||||
},
|
||||
"GITLAB$GET_TOKEN": {
|
||||
"en": "Generate a token on",
|
||||
"ja": "トークンを生成する",
|
||||
|
||||
@@ -15,11 +15,13 @@ import {
|
||||
} from "#/utils/custom-toast-handlers";
|
||||
import { retrieveAxiosErrorMessage } from "#/utils/retrieve-axios-error-message";
|
||||
import { GitSettingInputsSkeleton } from "#/components/features/settings/git-settings/github-settings-inputs-skeleton";
|
||||
import { useAuth } from "#/context/auth-context";
|
||||
|
||||
function GitSettingsScreen() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { mutate: saveSettings, isPending } = useSaveSettings();
|
||||
const { providerTokensSet } = useAuth();
|
||||
const { mutate: disconnectGitTokens } = useLogout();
|
||||
|
||||
const { data: settings, isLoading } = useSettings();
|
||||
@@ -29,10 +31,17 @@ function GitSettingsScreen() {
|
||||
React.useState(false);
|
||||
const [gitlabTokenInputHasValue, setGitlabTokenInputHasValue] =
|
||||
React.useState(false);
|
||||
const [githubBaseDomainInputHasValue, setGithubBaseDomainInputHasValue] =
|
||||
React.useState(false);
|
||||
const [gitlabBaseDomainInputHasValue, setGitlabBaseDomainInputHasValue] =
|
||||
React.useState(false);
|
||||
|
||||
const isSaas = config?.APP_MODE === "saas";
|
||||
const isGitHubTokenSet = !!settings?.PROVIDER_TOKENS_SET.github;
|
||||
const isGitLabTokenSet = !!settings?.PROVIDER_TOKENS_SET.gitlab;
|
||||
const isGitHubTokenSet = providerTokensSet.includes("github");
|
||||
const isGitLabTokenSet = providerTokensSet.includes("gitlab");
|
||||
|
||||
const existingGithubBaseDomain = settings?.PROVIDER_TOKENS_SET["github"];
|
||||
const existingGitlabBaseDomain = settings?.PROVIDER_TOKENS_SET["gitlab"];
|
||||
|
||||
const formAction = async (formData: FormData) => {
|
||||
const disconnectButtonClicked =
|
||||
@@ -45,12 +54,22 @@ function GitSettingsScreen() {
|
||||
|
||||
const githubToken = formData.get("github-token-input")?.toString() || "";
|
||||
const gitlabToken = formData.get("gitlab-token-input")?.toString() || "";
|
||||
const githubBaseDomain =
|
||||
formData.get("github-base-domain-input")?.toString() || "";
|
||||
const gitlabBaseDomain =
|
||||
formData.get("gitlab-base-domain-input")?.toString() || "";
|
||||
|
||||
saveSettings(
|
||||
{
|
||||
provider_tokens: {
|
||||
github: githubToken,
|
||||
gitlab: gitlabToken,
|
||||
github: {
|
||||
token: githubToken,
|
||||
base_domain: githubBaseDomain || null,
|
||||
},
|
||||
gitlab: {
|
||||
token: gitlabToken,
|
||||
base_domain: gitlabBaseDomain || null,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -64,12 +83,19 @@ function GitSettingsScreen() {
|
||||
onSettled: () => {
|
||||
setGithubTokenInputHasValue(false);
|
||||
setGitlabTokenInputHasValue(false);
|
||||
setGithubBaseDomainInputHasValue(false);
|
||||
setGitlabBaseDomainInputHasValue(false);
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const formIsClean = !githubTokenInputHasValue && !gitlabTokenInputHasValue;
|
||||
const formIsClean =
|
||||
!githubTokenInputHasValue &&
|
||||
!gitlabTokenInputHasValue &&
|
||||
!githubBaseDomainInputHasValue &&
|
||||
!gitlabBaseDomainInputHasValue;
|
||||
|
||||
const shouldRenderExternalConfigureButtons = isSaas && config.APP_SLUG;
|
||||
|
||||
return (
|
||||
@@ -84,22 +110,32 @@ function GitSettingsScreen() {
|
||||
<ConfigureGitHubRepositoriesAnchor slug={config.APP_SLUG!} />
|
||||
)}
|
||||
|
||||
{!isSaas && !isLoading && (
|
||||
{!isLoading && (
|
||||
<div className="p-9 flex flex-col gap-12">
|
||||
<GitHubTokenInput
|
||||
name="github-token-input"
|
||||
baseDomainSet={existingGithubBaseDomain}
|
||||
isGitHubTokenSet={isGitHubTokenSet}
|
||||
onChange={(value) => {
|
||||
setGithubTokenInputHasValue(!!value);
|
||||
}}
|
||||
onBaseDomainChange={(value) => {
|
||||
setGithubBaseDomainInputHasValue(!!value);
|
||||
}}
|
||||
isSaas={isSaas}
|
||||
/>
|
||||
|
||||
<GitLabTokenInput
|
||||
name="gitlab-token-input"
|
||||
baseDomainSet={existingGitlabBaseDomain}
|
||||
isGitLabTokenSet={isGitLabTokenSet}
|
||||
onChange={(value) => {
|
||||
setGitlabTokenInputHasValue(!!value);
|
||||
}}
|
||||
onBaseDomainChange={(value) => {
|
||||
setGitlabBaseDomainInputHasValue(!!value);
|
||||
}}
|
||||
isSaas={isSaas}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -11,13 +11,13 @@ export const DEFAULT_SETTINGS: Settings = {
|
||||
CONFIRMATION_MODE: false,
|
||||
SECURITY_ANALYZER: "",
|
||||
REMOTE_RUNTIME_RESOURCE_FACTOR: 1,
|
||||
PROVIDER_TOKENS_SET: { github: false, gitlab: false },
|
||||
PROVIDER_TOKENS_SET: { github: null, gitlab: null },
|
||||
ENABLE_DEFAULT_CONDENSER: true,
|
||||
ENABLE_SOUND_NOTIFICATIONS: false,
|
||||
USER_CONSENTS_TO_ANALYTICS: false,
|
||||
PROVIDER_TOKENS: {
|
||||
github: "",
|
||||
gitlab: "",
|
||||
github: { token: "", base_domain: null },
|
||||
gitlab: { token: "", base_domain: null },
|
||||
},
|
||||
IS_NEW_USER: true,
|
||||
};
|
||||
|
||||
@@ -5,6 +5,11 @@ export const ProviderOptions = {
|
||||
|
||||
export type Provider = keyof typeof ProviderOptions;
|
||||
|
||||
export type ProviderToken = {
|
||||
token: string;
|
||||
base_domain: string | null;
|
||||
};
|
||||
|
||||
export type Settings = {
|
||||
LLM_MODEL: string;
|
||||
LLM_BASE_URL: string;
|
||||
@@ -14,11 +19,11 @@ export type Settings = {
|
||||
CONFIRMATION_MODE: boolean;
|
||||
SECURITY_ANALYZER: string;
|
||||
REMOTE_RUNTIME_RESOURCE_FACTOR: number | null;
|
||||
PROVIDER_TOKENS_SET: Record<Provider, boolean>;
|
||||
PROVIDER_TOKENS_SET: Record<Provider, string | null>;
|
||||
ENABLE_DEFAULT_CONDENSER: boolean;
|
||||
ENABLE_SOUND_NOTIFICATIONS: boolean;
|
||||
USER_CONSENTS_TO_ANALYTICS: boolean | null;
|
||||
PROVIDER_TOKENS: Record<Provider, string>;
|
||||
PROVIDER_TOKENS: Record<Provider, ProviderToken>;
|
||||
IS_NEW_USER?: boolean;
|
||||
};
|
||||
|
||||
@@ -35,17 +40,17 @@ export type ApiSettings = {
|
||||
enable_default_condenser: boolean;
|
||||
enable_sound_notifications: boolean;
|
||||
user_consents_to_analytics: boolean | null;
|
||||
provider_tokens: Record<Provider, string>;
|
||||
provider_tokens_set: Record<Provider, boolean>;
|
||||
provider_tokens: Record<Provider, ProviderToken>;
|
||||
provider_tokens_set: Record<Provider, string | null>;
|
||||
};
|
||||
|
||||
export type PostSettings = Settings & {
|
||||
provider_tokens: Record<Provider, string>;
|
||||
provider_tokens: Record<Provider, ProviderToken>;
|
||||
user_consents_to_analytics: boolean | null;
|
||||
llm_api_key?: string | null;
|
||||
};
|
||||
|
||||
export type PostApiSettings = ApiSettings & {
|
||||
provider_tokens: Record<Provider, string>;
|
||||
provider_tokens: Record<Provider, ProviderToken>;
|
||||
user_consents_to_analytics: boolean | null;
|
||||
};
|
||||
|
||||
@@ -41,7 +41,7 @@ class GitHubService(BaseGitService, GitService):
|
||||
if token:
|
||||
self.token = token
|
||||
|
||||
if base_domain:
|
||||
if base_domain and base_domain != "github.com":
|
||||
self.BASE_URL = f'https://{base_domain}/api/v3'
|
||||
|
||||
@property
|
||||
|
||||
@@ -34,6 +34,7 @@ from openhands.server.types import AppMode
|
||||
class ProviderToken(BaseModel):
|
||||
token: SecretStr | None = Field(default=None)
|
||||
user_id: str | None = Field(default=None)
|
||||
base_domain: str | None = Field(default=None)
|
||||
|
||||
model_config = {
|
||||
'frozen': True, # Makes the entire model immutable
|
||||
@@ -43,15 +44,20 @@ class ProviderToken(BaseModel):
|
||||
@classmethod
|
||||
def from_value(cls, token_value: ProviderToken | dict[str, str]) -> ProviderToken:
|
||||
"""Factory method to create a ProviderToken from various input types"""
|
||||
if isinstance(token_value, ProviderToken):
|
||||
if isinstance(token_value, cls):
|
||||
return token_value
|
||||
elif isinstance(token_value, dict):
|
||||
token_str = token_value.get('token')
|
||||
user_id = token_value.get('user_id')
|
||||
return cls(token=SecretStr(token_str), user_id=user_id)
|
||||
base_domain = token_value.get('base_domain')
|
||||
return cls(
|
||||
token=SecretStr(token_str) if token_str is not None else None,
|
||||
user_id=user_id,
|
||||
base_domain=base_domain,
|
||||
)
|
||||
|
||||
else:
|
||||
raise ValueError('Unsupport Provider token type')
|
||||
raise ValueError('Unsupported Provider token type')
|
||||
|
||||
|
||||
PROVIDER_TOKEN_TYPE = MappingProxyType[ProviderType, ProviderToken]
|
||||
@@ -98,6 +104,7 @@ class SecretStore(BaseModel):
|
||||
if expose_secrets
|
||||
else pydantic_encoder(provider_token.token),
|
||||
'user_id': provider_token.user_id,
|
||||
'base_domain': provider_token.base_domain,
|
||||
}
|
||||
|
||||
return tokens
|
||||
|
||||
@@ -20,7 +20,6 @@ from openhands.server.shared import config
|
||||
from openhands.storage.data_models.settings import Settings
|
||||
from openhands.server.user_auth import (
|
||||
get_provider_tokens,
|
||||
get_user_id,
|
||||
get_user_settings,
|
||||
get_user_settings_store,
|
||||
)
|
||||
@@ -31,7 +30,6 @@ app = APIRouter(prefix='/api')
|
||||
|
||||
@app.get('/settings', response_model=GETSettingsModel)
|
||||
async def load_settings(
|
||||
user_id: str | None = Depends(get_user_id),
|
||||
provider_tokens: PROVIDER_TOKEN_TYPE | None = Depends(get_provider_tokens),
|
||||
settings: Settings | None = Depends(get_user_settings),
|
||||
) -> GETSettingsModel | JSONResponse:
|
||||
@@ -43,18 +41,10 @@ async def load_settings(
|
||||
)
|
||||
|
||||
provider_tokens_set = {}
|
||||
|
||||
if bool(user_id):
|
||||
provider_tokens_set[ProviderType.GITHUB.value] = True
|
||||
|
||||
if provider_tokens:
|
||||
all_provider_types = [provider.value for provider in ProviderType]
|
||||
provider_tokens_types = [provider.value for provider in provider_tokens]
|
||||
for provider_type in all_provider_types:
|
||||
if provider_type in provider_tokens_types:
|
||||
provider_tokens_set[provider_type] = True
|
||||
else:
|
||||
provider_tokens_set[provider_type] = False
|
||||
for provider_type, provider_token in provider_tokens.items():
|
||||
if provider_token.token or provider_token.user_id:
|
||||
provider_tokens_set[provider_type] = provider_token.base_domain
|
||||
|
||||
settings_with_token_data = GETSettingsModel(
|
||||
**settings.model_dump(exclude='secrets_store'),
|
||||
@@ -218,66 +208,80 @@ async def reset_settings() -> JSONResponse:
|
||||
)
|
||||
|
||||
|
||||
async def check_provider_tokens(settings: POSTSettingsModel) -> str:
|
||||
async def check_provider_tokens(settings: POSTSettingsModel, existing_settings: Settings | None) -> str:
|
||||
if settings.provider_tokens:
|
||||
# Remove extraneous token types
|
||||
provider_types = [provider.value for provider in ProviderType]
|
||||
provider_types = [provider for provider in ProviderType]
|
||||
settings.provider_tokens = {
|
||||
k: v for k, v in settings.provider_tokens.items() if k in provider_types
|
||||
}
|
||||
|
||||
# Determine whether tokens are valid
|
||||
for token_type, token_value in settings.provider_tokens.items():
|
||||
if token_value:
|
||||
confirmed_token_type = await validate_provider_token(
|
||||
SecretStr(token_value)
|
||||
)
|
||||
if not confirmed_token_type or confirmed_token_type.value != token_type:
|
||||
return f'Invalid token. Please make sure it is a valid {token_type} token.'
|
||||
for provider_type, provider_token in settings.provider_tokens.items():
|
||||
token_value = provider_token
|
||||
existing_token = existing_settings.secrets_store.provider_tokens.get(provider_type, None) if existing_settings else None
|
||||
|
||||
# Use incoming value otherwise default to existing value
|
||||
token = SecretStr("")
|
||||
if token_value.token:
|
||||
token = token_value.token
|
||||
elif existing_token and existing_token.token:
|
||||
token = existing_token.token
|
||||
|
||||
if not token:
|
||||
continue
|
||||
|
||||
base_domain = provider_token.base_domain # FE should always send latest base_domain param
|
||||
confirmed_token_type = await validate_provider_token(
|
||||
token,
|
||||
base_domain
|
||||
)
|
||||
|
||||
|
||||
if not confirmed_token_type or confirmed_token_type != provider_type:
|
||||
return f'Invalid {provider_type.value} token or base domain.'
|
||||
|
||||
return ''
|
||||
|
||||
|
||||
async def store_provider_tokens(
|
||||
settings: POSTSettingsModel, settings_store: SettingsStore
|
||||
settings: POSTSettingsModel, existing_settings: Settings
|
||||
):
|
||||
existing_settings = await settings_store.load()
|
||||
if existing_settings:
|
||||
if settings.provider_tokens:
|
||||
if existing_settings.secrets_store:
|
||||
existing_providers = [
|
||||
provider.value
|
||||
for provider in existing_settings.secrets_store.provider_tokens
|
||||
]
|
||||
existing_providers = [
|
||||
provider
|
||||
for provider in existing_settings.secrets_store.provider_tokens
|
||||
]
|
||||
|
||||
# Merge incoming settings store with the existing one
|
||||
for provider, token_value in list(settings.provider_tokens.items()):
|
||||
if provider in existing_providers and not token_value:
|
||||
provider_type = ProviderType(provider)
|
||||
existing_token = (
|
||||
existing_settings.secrets_store.provider_tokens.get(
|
||||
provider_type
|
||||
)
|
||||
# Merge incoming settings store with the existing one
|
||||
for provider, token_value in settings.provider_tokens.items():
|
||||
if provider in existing_providers and not token_value.token:
|
||||
provider_type = ProviderType(provider)
|
||||
existing_token = (
|
||||
existing_settings.secrets_store.provider_tokens.get(
|
||||
provider_type
|
||||
)
|
||||
if existing_token and existing_token.token:
|
||||
settings.provider_tokens[provider] = (
|
||||
existing_token.token.get_secret_value()
|
||||
)
|
||||
)
|
||||
|
||||
if existing_token:
|
||||
updated_token = ProviderToken(
|
||||
token=existing_token.token,
|
||||
user_id=existing_token.user_id,
|
||||
base_domain=token_value.base_domain
|
||||
)
|
||||
|
||||
settings.provider_tokens[provider] = updated_token
|
||||
|
||||
else: # nothing passed in means keep current settings
|
||||
provider_tokens = existing_settings.secrets_store.provider_tokens
|
||||
settings.provider_tokens = {
|
||||
provider.value: data.token.get_secret_value() if data.token else None
|
||||
for provider, data in provider_tokens.items()
|
||||
}
|
||||
settings.provider_tokens = dict(existing_settings.secrets_store.provider_tokens)
|
||||
|
||||
return settings
|
||||
|
||||
|
||||
async def store_llm_settings(
|
||||
settings: POSTSettingsModel, settings_store: SettingsStore
|
||||
settings: POSTSettingsModel, existing_settings: Settings
|
||||
) -> POSTSettingsModel:
|
||||
existing_settings = await settings_store.load()
|
||||
|
||||
# Convert to Settings model and merge with existing settings
|
||||
if existing_settings:
|
||||
# Keep existing LLM settings if not provided
|
||||
@@ -295,9 +299,10 @@ async def store_llm_settings(
|
||||
async def store_settings(
|
||||
settings: POSTSettingsModel,
|
||||
settings_store: SettingsStore = Depends(get_user_settings_store),
|
||||
existing_settings: Settings | None = Depends(get_user_settings),
|
||||
) -> JSONResponse:
|
||||
# Check provider tokens are valid
|
||||
provider_err_msg = await check_provider_tokens(settings)
|
||||
provider_err_msg = await check_provider_tokens(settings, existing_settings)
|
||||
if provider_err_msg:
|
||||
return JSONResponse(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
@@ -305,11 +310,9 @@ async def store_settings(
|
||||
)
|
||||
|
||||
try:
|
||||
existing_settings = await settings_store.load()
|
||||
|
||||
# Convert to Settings model and merge with existing settings
|
||||
if existing_settings:
|
||||
settings = await store_llm_settings(settings, settings_store)
|
||||
settings = await store_llm_settings(settings, existing_settings)
|
||||
|
||||
# Keep existing analytics consent if not provided
|
||||
if settings.user_consents_to_analytics is None:
|
||||
@@ -317,7 +320,7 @@ async def store_settings(
|
||||
existing_settings.user_consents_to_analytics
|
||||
)
|
||||
|
||||
settings = await store_provider_tokens(settings, settings_store)
|
||||
settings = await store_provider_tokens(settings, existing_settings)
|
||||
|
||||
# Update sandbox config with new settings
|
||||
if settings.remote_runtime_resource_factor is not None:
|
||||
@@ -357,17 +360,9 @@ def convert_to_settings(settings_with_token_data: POSTSettingsModel) -> Settings
|
||||
|
||||
# Create new provider tokens immutably
|
||||
if settings_with_token_data.provider_tokens:
|
||||
tokens = {}
|
||||
for token_type, token_value in settings_with_token_data.provider_tokens.items():
|
||||
if token_value:
|
||||
provider = ProviderType(token_type)
|
||||
tokens[provider] = ProviderToken(
|
||||
token=SecretStr(token_value), user_id=None
|
||||
)
|
||||
|
||||
# Create new SecretStore with tokens
|
||||
settings = settings.model_copy(
|
||||
update={'secrets_store': SecretStore(provider_tokens=tokens)}
|
||||
update={'secrets_store': SecretStore(provider_tokens=settings_with_token_data.provider_tokens)}
|
||||
)
|
||||
|
||||
return settings
|
||||
|
||||
@@ -5,6 +5,8 @@ from pydantic import (
|
||||
SecretStr,
|
||||
)
|
||||
|
||||
from openhands.integrations.provider import ProviderToken
|
||||
from openhands.integrations.service_types import ProviderType
|
||||
from openhands.storage.data_models.settings import Settings
|
||||
|
||||
|
||||
@@ -13,7 +15,7 @@ class POSTSettingsModel(Settings):
|
||||
Settings for POST requests
|
||||
"""
|
||||
|
||||
provider_tokens: dict[str, str] = {}
|
||||
provider_tokens: dict[ProviderType, ProviderToken] = {}
|
||||
|
||||
|
||||
class POSTSettingsCustomSecrets(BaseModel):
|
||||
@@ -29,9 +31,14 @@ class GETSettingsModel(Settings):
|
||||
Settings with additional token data for the frontend
|
||||
"""
|
||||
|
||||
provider_tokens_set: dict[str, bool] | None = None
|
||||
provider_tokens_set: dict[ProviderType, str | None] | None = (
|
||||
None # Provider Type and base domain key-value pair
|
||||
)
|
||||
llm_api_key_set: bool
|
||||
|
||||
class Config:
|
||||
use_enum_values = True
|
||||
|
||||
|
||||
class GETSettingsCustomSecrets(BaseModel):
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user