Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
20 changes: 4 additions & 16 deletions .github/workflows/branch-policy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,7 @@ name: Enforce branch flow
on:
pull_request:
branches:
- stable
- main
- 'main/**'
- master

jobs:
check-branch-policy:
Expand All @@ -17,19 +15,9 @@ jobs:

echo "PR: $SOURCE → $TARGET"

# stable ← main, main/*, hotfix/*
# main ← develop, hotfix/*
# main/* ← develop, hotfix/*
if [[ "$TARGET" == "stable" ]]; then
PATTERN="^(main(/.*)?|hotfix/.+)$"
ALLOWED="main, main/*, hotfix/*"
elif [[ "$TARGET" == "main" || "$TARGET" == main/* ]]; then
PATTERN="^(develop|hotfix/.+)$"
ALLOWED="develop, hotfix/*"
else
echo "✅ No branch policy for target '$TARGET'"
exit 0
fi
# master ← develop, hotfix/*
PATTERN="^(develop|hotfix/.+)$"
ALLOWED="develop, hotfix/*"

if [[ "$SOURCE" =~ $PATTERN ]]; then
echo "✅ '$SOURCE' → '$TARGET' is allowed"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
name: Bump version on PR to stable
name: Bump version on PR to master

on:
pull_request:
types: [opened, synchronize]
branches: [stable]
branches: [master]

jobs:
bump-version:
# Only run for main/* branches
if: startsWith(github.head_ref, 'main/')
# Only run for the develop → master release train.
# Hotfix branches bump their version manually before opening the PR.
if: github.head_ref == 'develop'
runs-on: ubuntu-latest
permissions:
contents: write
Expand All @@ -23,20 +24,21 @@ jobs:
- name: Read versions and compare
id: compare
run: |
# Read version from source branch (main/*)
# Read version from source branch (develop)
SOURCE_VERSION=$(grep -m1 '^version' pyproject.toml | sed 's/.*"\(.*\)"/\1/')
echo "source=$SOURCE_VERSION" >> "$GITHUB_OUTPUT"

# Read version from stable
git fetch origin stable
STABLE_VERSION=$(git show origin/stable:pyproject.toml | grep -m1 '^version' | sed 's/.*"\(.*\)"/\1/')
echo "stable=$STABLE_VERSION" >> "$GITHUB_OUTPUT"
# Read version from master
git fetch origin master
MASTER_VERSION=$(git show origin/master:pyproject.toml | grep -m1 '^version' | sed 's/.*"\(.*\)"/\1/')
echo "master=$MASTER_VERSION" >> "$GITHUB_OUTPUT"

echo "Source: $SOURCE_VERSION | Stable: $STABLE_VERSION"
echo "Source: $SOURCE_VERSION | Master: $MASTER_VERSION"

# Compare using sort -V (version sort)
HIGHER=$(printf '%s\n%s' "$SOURCE_VERSION" "$STABLE_VERSION" | sort -V | tail -1)
if [ "$SOURCE_VERSION" != "$STABLE_VERSION" ] && [ "$SOURCE_VERSION" = "$HIGHER" ]; then
# Compare using sort -V (version sort). If develop is already ahead
# (e.g. a manual minor/major bump), no patch bump is needed.
HIGHER=$(printf '%s\n%s' "$SOURCE_VERSION" "$MASTER_VERSION" | sort -V | tail -1)
if [ "$SOURCE_VERSION" != "$MASTER_VERSION" ] && [ "$SOURCE_VERSION" = "$HIGHER" ]; then
echo "needs_bump=false" >> "$GITHUB_OUTPUT"
echo "Source is already ahead, no bump needed"
else
Expand All @@ -48,7 +50,7 @@ jobs:
if: steps.compare.outputs.needs_bump == 'true'
id: next
run: |
IFS='.' read -r MAJOR MINOR PATCH <<< "${{ steps.compare.outputs.stable }}"
IFS='.' read -r MAJOR MINOR PATCH <<< "${{ steps.compare.outputs.master }}"
PATCH=$((PATCH + 1))
echo "version=$MAJOR.$MINOR.$PATCH" >> "$GITHUB_OUTPUT"
echo "Next version: $MAJOR.$MINOR.$PATCH"
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/pr-lint-and-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ name: PR Lint & Test

on:
pull_request:
branches: [develop, stable, main, 'main/**']
branches: [develop, master]
workflow_dispatch:

jobs:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
name: Release on merge to stable
name: Release on merge to master

on:
pull_request:
types: [closed]
branches: [stable]
branches: [master]

jobs:
release:
Expand All @@ -13,10 +13,10 @@ jobs:
contents: write

steps:
- name: Checkout stable
- name: Checkout master
uses: actions/checkout@v4
with:
ref: stable
ref: master
fetch-depth: 0

- name: Read version from pyproject.toml
Expand All @@ -37,4 +37,4 @@ jobs:
fi
git tag "$TAG"
git push origin "$TAG"
gh release create "$TAG" --title "$TAG" --generate-notes --target stable
gh release create "$TAG" --title "$TAG" --generate-notes --target master
16 changes: 16 additions & 0 deletions dana/common/llm/providers/anthropic.py
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,22 @@ async def chat(self, messages: list[LLMMessage], tools: list[dict] | None = None

except anthropic.APITimeoutError as e:
raise LLMTimeoutError(f"Anthropic API timeout: {e}") from e
except anthropic.BadRequestError as e:
# Map Anthropic prompt-too-long to typed PromptTooLongError so the
# caller-layer (llm_caller.py) can trigger reactive_compact + retry.
from dana.common.llm.types import PromptTooLongError

err_body: dict = {}
try:
err_body = (e.response.json() or {}).get("error", {}) if hasattr(e, "response") else {}
except Exception:
err_body = {}
err_type = err_body.get("type")
err_msg = err_body.get("message", "") or str(e)
if err_type == "invalid_request_error" and "prompt is too long" in err_msg.lower():
raise PromptTooLongError(f"Anthropic: {err_msg}") from e
logger.error("Anthropic API error", error=str(e))
raise
except Exception as e:
logger.error("Anthropic API error", error=str(e))
raise
Expand Down
32 changes: 31 additions & 1 deletion dana/common/llm/providers/azure.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import structlog

from ...config import config_manager
from .openai_compatible_base import OpenAICompatibleProvider
from .openai_compatible_base import OpenAICompatibleProvider, make_logging_http_client


logger = structlog.get_logger()
Expand All @@ -13,6 +13,34 @@
class AzureProvider(OpenAICompatibleProvider):
"""Azure OpenAI provider."""

# Azure exposes the Responses API only on api-version >= this date.
# Older versions return HTTP 400 BadRequest for /openai/responses.
_RESPONSES_API_MIN_DATE = "2025-03-01"

# Env var operators use to dial reasoning effort for Azure deployments.
# Valid values: "minimal" | "low" | "medium" | "high".
_REASONING_EFFORT_ENV_VAR = "AZURE_THINKING_EFFORT"

# Env var to force/disable the Responses API for this provider.
# Truthy: 1/true/on/yes — falsy: 0/false/off/no. Overrides the config flag
# and the api-version gate (forcing on an old api-version will likely 400).
_RESPONSES_API_ENV_VAR = "AZURE_USE_RESPONSES_API"

@property
def name(self) -> str:
return "azure"

def _endpoint_url(self) -> str:
return getattr(self, "azure_endpoint", "") or ""

def _responses_api_supported(self) -> bool:
# api-version format is "YYYY-MM-DD" or "YYYY-MM-DD-preview"; first 10 chars
# are the ISO date which sorts correctly lexicographically.
version = getattr(self, "api_version", None)
if not version or len(version) < 10:
return False
return version[:10] >= self._RESPONSES_API_MIN_DATE

def __init__(
self, api_key: str | None = None, model: str = "gpt-35-turbo", base_url: str | None = None, api_version: str | None = None
):
Expand All @@ -38,6 +66,7 @@ def __init__(
raise ValueError("Azure OpenAI endpoint URL not found. Set AZURE_OPENAI_API_URL environment variable.")

azure_endpoint = azure_endpoint.rstrip("/")
self.azure_endpoint = azure_endpoint

if api_version:
self.api_version = api_version
Expand All @@ -48,6 +77,7 @@ def __init__(
api_key=self.api_key,
azure_endpoint=azure_endpoint,
api_version=self.api_version,
http_client=make_logging_http_client(self.DEFAULT_TIMEOUT_SECONDS),
)

# Check for use_responses_api config flag
Expand Down
11 changes: 11 additions & 0 deletions dana/common/llm/providers/gemini.py
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,17 @@ async def chat(self, messages: list[LLMMessage], tools: list | None = None, **kw
if fr:
finish_reason = str(fr)

# Gemini silently truncates over-budget prompts via MAX_TOKENS finish
# reason — there is no SDK PTL error. Best-effort WARNING log only;
# reactive_compact cannot run here (indistinguishable from output-limit
# truncation). Ops must tune DANA_COMPACT_TRIGGER_TOKENS conservatively.
if finish_reason and "MAX_TOKENS" in finish_reason:
logger.warning(
"gemini_max_tokens_finish",
model=self.model,
note="may indicate context-window overflow; tune DANA_COMPACT_TRIGGER_TOKENS",
)

return LLMResponse(
content=content,
model=self.model,
Expand Down
12 changes: 7 additions & 5 deletions dana/common/llm/providers/moonshot.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

from ...config import config_manager
from ..types import read_media_as_base64, unsupported_placeholder
from .openai_compatible_base import OpenAICompatibleProvider
from .openai_compatible_base import OpenAICompatibleProvider, make_logging_http_client


logger = structlog.get_logger()
Expand All @@ -36,11 +36,13 @@ def __init__(self, api_key: str | None = None, model: str = "kimi-k2.5", base_ur
if base_url:
self.base_url = base_url
else:
self.base_url = (
config_manager.get_provider_base_url(MOONSHOT_PROVIDER_NAME) or "https://api.moonshot.ai/v1"
)
self.base_url = config_manager.get_provider_base_url(MOONSHOT_PROVIDER_NAME) or "https://api.moonshot.ai/v1"

self.client = AsyncOpenAI(api_key=self.api_key, base_url=self.base_url)
self.client = AsyncOpenAI(
api_key=self.api_key,
base_url=self.base_url,
http_client=make_logging_http_client(self.DEFAULT_TIMEOUT_SECONDS),
)

# Moonshot never uses Responses API
self._use_responses_api = False
Expand Down
23 changes: 21 additions & 2 deletions dana/common/llm/providers/openai.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import structlog

from ...config import config_manager
from .openai_compatible_base import OpenAICompatibleProvider
from .openai_compatible_base import OpenAICompatibleProvider, make_logging_http_client


logger = structlog.get_logger()
Expand All @@ -13,6 +13,21 @@
class OpenAIProvider(OpenAICompatibleProvider):
"""OpenAI API provider."""

# Env var operators use to dial reasoning effort for OpenAI direct API.
# Valid values: "minimal" | "low" | "medium" | "high".
_REASONING_EFFORT_ENV_VAR = "OPENAI_THINKING_EFFORT"

# Env var to force/disable the Responses API for this provider.
# Truthy: 1/true/on/yes — falsy: 0/false/off/no. Overrides the config flag.
_RESPONSES_API_ENV_VAR = "OPENAI_USE_RESPONSES_API"

@property
def name(self) -> str:
return "openai"

def _endpoint_url(self) -> str:
return getattr(self, "base_url", None) or "https://api.openai.com/v1"

def __init__(self, api_key: str | None = None, model: str = "gpt-3.5-turbo", base_url: str | None = None):
self.model = model

Expand All @@ -31,7 +46,11 @@ def __init__(self, api_key: str | None = None, model: str = "gpt-3.5-turbo", bas
else:
self.base_url = config_manager.get_provider_base_url("openai")

self.client = AsyncOpenAI(api_key=self.api_key, base_url=self.base_url)
self.client = AsyncOpenAI(
api_key=self.api_key,
base_url=self.base_url,
http_client=make_logging_http_client(self.DEFAULT_TIMEOUT_SECONDS),
)

# Check for use_responses_api config flag
provider_config = config_manager.get_provider_config("openai")
Expand Down
Loading
Loading