Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
ad37f48
feat(llm): add native Databricks Foundation Model API provider
prasadkona Apr 21, 2026
0c09e52
fix(llm): add check_fields=False to safety_settings deprecation valid…
prasadkona May 16, 2026
f452785
fix(llm): pop 'stream' from kwargs in _transport_call to avoid duplic…
prasadkona May 16, 2026
b6404fd
fix(llm): strip litellm kwargs from _transport_call before AI Gateway…
prasadkona May 17, 2026
a47331b
fix(databricks): serialize databricks_client_secret as plaintext on save
prasadkona May 17, 2026
33d4e0c
docs(databricks): remove internal _local/ reference from provider README
prasadkona May 17, 2026
b383c08
fix(auth): use split instead of replace to extract Bearer token value
prasadkona May 17, 2026
097b6ee
fix: eagerly register DatabricksLLM subclass and add skills compat shims
prasadkona May 18, 2026
efcd3bb
fix(tests): update UA test assertions for OpenHandsOSS/ prefix rename
prasadkona May 18, 2026
3c0ecc6
feat: add databricks_u2m_client_id and databricks_u2m_redirect_uri fi…
prasadkona May 23, 2026
cc488ac
feat: persist databricks_u2m_client_secret for confidential OAuth apps
prasadkona May 23, 2026
323b7e1
fix(databricks): resolve HIGH/MEDIUM auth issues - client secret forw…
prasadkona May 23, 2026
e9a72c7
chore(databricks): sync curated model list and context tables to May …
prasadkona May 23, 2026
fc65a0e
fix(databricks): add error hints and update curated model list
prasadkona May 24, 2026
a743420
Merge remote-tracking branch 'origin/main' into feat/databricks-nativ…
prasadkona May 25, 2026
a61c9e0
Merge remote-tracking branch 'origin/main' into feat/databricks-nativ…
prasadkona Jun 6, 2026
fc0f735
fix(sdk): fix ModelFeatures init signature and graceful permission er…
prasadkona Jun 7, 2026
a578ce9
docs(databricks): minor doc updates
prasadkona Jun 7, 2026
a1c288d
refactor(databricks): remove out-of-scope changes from connector PR
prasadkona Jun 7, 2026
592f844
feat(databricks): add shared U2M PKCE helpers consumed by web + CLI
prasadkona Jun 7, 2026
848bda3
test(databricks): drop stale model cases and refresh curated assertions
prasadkona Jun 7, 2026
90a1a69
docs(databricks): document pkce + settings_bridge modules and shared …
prasadkona Jun 7, 2026
9d058e5
docs(databricks): note alignment with Databricks ucode credential model
prasadkona Jun 8, 2026
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
22 changes: 22 additions & 0 deletions openhands-sdk/openhands/sdk/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,27 @@
_print_banner(__version__)


def create_llm(model: str = "", **kwargs) -> LLM:
"""Factory function that routes to the correct LLM subclass.

Routes models with the "databricks/" prefix to DatabricksLLM (native FMAPI
provider, bypassing LiteLLM). All other models use the base LLM class.

Uses lazy import for DatabricksLLM to avoid circular import and to keep
Databricks dependencies optional (install via: pip install openhands-sdk[databricks]).

Example:
llm = create_llm("databricks/databricks-meta-llama-3-3-70b-instruct",
databricks_host="https://adb-xxx.azuredatabricks.net",
api_key=SecretStr("dapi..."))
llm = create_llm("claude-sonnet-4-20250514", api_key=SecretStr("sk-ant-..."))
"""
if model.startswith("databricks/"):
from openhands.sdk.llm.providers.databricks.llm import DatabricksLLM

return DatabricksLLM(model=model, **kwargs)
return LLM(model=model, **kwargs)

__all__ = [
"LLM",
"LLM_PROFILE_SCHEMA_VERSION",
Expand Down Expand Up @@ -203,4 +224,5 @@
"load_user_skills",
"page_iterator",
"__version__",
"create_llm",
]
5 changes: 3 additions & 2 deletions openhands-sdk/openhands/sdk/agent/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from abc import ABC, abstractmethod
from collections.abc import Generator, Iterable, Sequence
from concurrent.futures import ThreadPoolExecutor
from typing import TYPE_CHECKING, Any, Literal
from typing import TYPE_CHECKING, Annotated, Any, Literal

from pydantic import (
BaseModel,
Expand All @@ -17,6 +17,7 @@
PrivateAttr,
SecretStr,
SerializationInfo,
SerializeAsAny,
ValidationInfo,
model_serializer,
model_validator,
Expand Down Expand Up @@ -110,7 +111,7 @@ class AgentBase(DiscriminatedUnionMixin, ABC):
arbitrary_types_allowed=True,
)

llm: LLM = Field(
llm: Annotated[LLM, SerializeAsAny()] = Field(
...,
description="LLM configuration for the agent.",
examples=[
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import os
from collections.abc import Sequence
from enum import Enum
from typing import Annotated

from pydantic import Field, model_validator
from pydantic import Field, SerializeAsAny, model_validator

from openhands.sdk.context.condenser.base import (
CondensationRequirement,
Expand Down Expand Up @@ -43,7 +44,7 @@ class LLMSummarizingCondenser(RollingCondenser):
it is the same as the one defined in this condenser.
"""

llm: LLM
llm: Annotated[LLM, SerializeAsAny()]
max_size: int = Field(default=240, gt=0)
max_tokens: int | None = None

Expand Down
75 changes: 75 additions & 0 deletions openhands-sdk/openhands/sdk/event/conversation_error.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,78 @@
import re

from pydantic import Field
from rich.text import Text

from openhands.sdk.event.base import Event


# ---------------------------------------------------------------------------
# Hint rules: list of (pattern, hint_text) pairs. The first matching pattern
# wins. Patterns are matched case-insensitively against ``detail``.
# ---------------------------------------------------------------------------
_HINT_RULES: list[tuple[re.Pattern[str], str]] = [
# Databricks AI Gateway: endpoint not found in workspace (404).
# Typically means cross-geography routing is disabled, or the endpoint
# has not been deployed in this workspace.
(
re.compile(
r"\[404\]\s*AI\s+Gateway\s+endpoint\s+['\"]?(\S+?)['\"]?\s+does\s+not\s+exist",
re.IGNORECASE,
),
(
"This Databricks endpoint is not available in your workspace.\n"
"Possible reasons:\n"
" • The model requires cross-geography routing, which is not\n"
" enabled in your workspace (contact your admin).\n"
" • The endpoint name is misspelled or not yet deployed.\n"
"Tip: Open Settings → click 'Refresh Models' to see the endpoints\n"
"that are actually available in your workspace, then save a\n"
"different model."
),
),
# Databricks: org-level access denied (403 Invalid access to Org).
# Gemini and other cross-geography models route through a Databricks
# global GCP org. The 403 means that routing is not enabled for this
# workspace account.
(
re.compile(r"\[403\].*Invalid\s+access\s+to\s+Org", re.IGNORECASE),
(
"Your workspace does not have permission to access this model.\n"
"This error most commonly occurs with Gemini models, which require\n"
"cross-geography routing through a Databricks GCP organisation.\n"
"Action: ask your Databricks account admin to enable\n"
" 'Cross-geography model serving' for your account, or choose a\n"
" different model (Claude / Llama / DBRX) that runs within your\n"
" workspace region.\n"
"Tip: Open Settings → click '↻ Refresh Models' to see only the\n"
"endpoints available in your workspace, then pick a different model."
),
),
# Databricks: authentication failure (401 / token expired).
(
re.compile(r"\[401\].*databricks|databricks.*\[401\]|UNAUTHENTICATED", re.IGNORECASE),
(
"Databricks authentication failed.\n"
"Tip: Open Settings and re-authenticate (re-run the browser sign-in\n"
"for U2M, or verify your client credentials for M2M)."
),
),
# Generic LiteLLM / provider rate-limit.
(
re.compile(r"\[429\]|rate.?limit|too many requests", re.IGNORECASE),
"The model endpoint returned a rate-limit error. Wait a moment and retry.",
),
]


def _get_hint(detail: str) -> str | None:
"""Return the first matching hint for the given error detail, or None."""
for pattern, hint in _HINT_RULES:
if pattern.search(detail):
return hint
return None


class ConversationErrorEvent(Event):
"""
Conversation-level failure that is NOT sent back to the LLM.
Expand Down Expand Up @@ -34,4 +103,10 @@ def visualize(self) -> Text:
content.append(self.code)
content.append("\n\nDetail:\n", style="bold")
content.append(self.detail)

hint = _get_hint(self.detail)
if hint:
content.append("\n\nHint:\n", style="bold yellow")
content.append(hint, style="yellow")

return content
10 changes: 10 additions & 0 deletions openhands-sdk/openhands/sdk/llm/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,16 @@
)
from openhands.sdk.llm.utils.verified_models import VERIFIED_MODELS

# Eagerly import DatabricksLLM so it registers with LLM.__subclasses__() at
# module-load time. This ensures that LLM._dispatch_to_provider_subclass can
# reconstruct a DatabricksLLM when deserializing persisted agent JSON that
# carries provider="databricks" — even in processes (e.g. the agent server)
# that never explicitly import the Databricks provider.
try:
from openhands.sdk.llm.providers.databricks.llm import DatabricksLLM # noqa: F401
except Exception: # pragma: no cover — optional dependency
pass


__all__ = [
# Auth
Expand Down
34 changes: 34 additions & 0 deletions openhands-sdk/openhands/sdk/llm/llm.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@
BaseModel,
ConfigDict,
Field,
ModelWrapValidatorHandler,
PrivateAttr,
SecretStr,
ValidationInfo,
field_serializer,
field_validator,
model_validator,
Expand Down Expand Up @@ -509,6 +511,38 @@ class LLM(BaseModel, RetryMixin, NonNativeToolCallingMixin):
# =========================================================================
# Validators
# =========================================================================
@model_validator(mode="wrap")
@classmethod
def _dispatch_to_provider_subclass(
cls,
data: Any,
handler: ModelWrapValidatorHandler["LLM"],
info: ValidationInfo,
) -> "LLM":
"""Route persisted agent JSON to the matching LLM subclass.

When a saved agent is rehydrated the ``llm`` dict may carry a
``provider`` key written by a subclass (e.g. ``"provider":
"databricks"``). Without this, ``LLM.model_validate(data)`` would
build a base ``LLM`` and silently drop the subclass-only fields.

Only fires when called directly on the base ``LLM`` class with a
plain dict — subclass validators and non-dict inputs pass through
unchanged. Subclasses are discovered generically via
``__subclasses__()`` keyed off their ``provider`` Literal annotation,
so no provider names are hardcoded here.
"""
if cls is not LLM or not isinstance(data, dict):
return handler(data)
provider = data.get("provider")
if not provider:
return handler(data)
for sub in LLM.__subclasses__():
f = sub.model_fields.get("provider")
if f and provider in getattr(f.annotation, "__args__", ()):
return sub.model_validate(data, context=info.context)
return handler(data)

@field_validator(
"api_key", "aws_access_key_id", "aws_secret_access_key", "aws_session_token"
)
Expand Down
3 changes: 3 additions & 0 deletions openhands-sdk/openhands/sdk/llm/providers/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Provider implementations for the OpenHands V1 SDK LLM layer.
# Each provider subpackage contains a DatabricksLLM (or equivalent) subclass
# that bypasses LiteLLM for direct, PWAF-compliant API communication.
Loading