Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
7420dd6
Bump elysia from 1.4.27 to 1.4.29
dependabot[bot] Sep 23, 2026
377dc26
Bump urllib3 from 2.7.0 to 2.8.0
dependabot[bot] Oct 1, 2026
9b141bc
Bump the python-minor-patch group with 7 updates
dependabot[bot] Oct 2, 2026
dffc653
Bump openai from 2.48.0 to 3.20.0
dependabot[bot] Oct 2, 2026
8bb6961
Bump pandas from 2.3.3 to 3.0.6
dependabot[bot] Oct 2, 2026
aeefb2d
Bump anthropic from 0.119.0 to 1.9.0
dependabot[bot] Oct 2, 2026
26394e3
Merge remote-tracking branch 'origin/dependabot/npm_and_yarn/elysia-1…
hanna-paasivirta Oct 6, 2026
326d5fd
Merge remote-tracking branch 'origin/dependabot/pip/python-minor-patc…
hanna-paasivirta Oct 6, 2026
8f83ef7
Merge remote-tracking branch 'origin/dependabot/pip/urllib3-2.8.0' in…
hanna-paasivirta Oct 6, 2026
7a75149
Merge remote-tracking branch 'origin/dependabot/pip/anthropic-1.9.0' …
hanna-paasivirta Oct 6, 2026
b61aa96
Merge remote-tracking branch 'origin/dependabot/pip/openai-3.20.0' in…
hanna-paasivirta Oct 6, 2026
30730e2
Merge remote-tracking branch 'origin/dependabot/pip/pandas-3.0.6' int…
hanna-paasivirta Oct 6, 2026
01ddcf1
Sync bun.lock with elysia bump
hanna-paasivirta Oct 6, 2026
1a1f975
Use httpx2 to match anthropic 1.x
hanna-paasivirta Oct 8, 2026
2ba057a
Drop temperature, removed in anthropic 1.x
hanna-paasivirta Oct 8, 2026
5b356f4
Pick up the newer minor/patch bumps from #715
hanna-paasivirta Oct 12, 2026
0347252
Add changeset for dependency bumps
hanna-paasivirta Oct 13, 2026
594825c
Merge remote-tracking branch 'origin/main' into oct-dependency-bumps
hanna-paasivirta Oct 13, 2026
79a81ad
Use httpx2 in the web search planner tests
hanna-paasivirta Oct 13, 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
5 changes: 5 additions & 0 deletions .changeset/october-dependency-bumps.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"apollo": patch
---

Update dependencies, including the Anthropic Python SDK to 1.x, the OpenAI SDK to 3.x and pandas to 3.x
4 changes: 2 additions & 2 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
"@elysiajs/html": "^1.4.0",
"@openfn/adaptor-apis": "^0.3.0",
"@sentry/bun": "^10.60.0",
"elysia": "1.4.27",
"elysia": "1.4.29",
"jsdoc-babel": "^0.5.0"
},
"nodemonConfig": {
Expand Down
762 changes: 427 additions & 335 deletions poetry.lock

Large diffs are not rendered by default.

20 changes: 10 additions & 10 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ package-mode = false
requires-poetry = ">=2.3.2"

[tool.poetry.dependencies]
python = "3.11.*"
openai = "^2.48"
python-dotenv = "^1.2.3"
anthropic = "^0.119.0"
python = ">=3.11,<3.12"
openai = "^3.20"
python-dotenv = "^1.2.4"
anthropic = "^1.9.0"

langchain-pinecone = "^0.2.13"
langchain-core = "^1.6"
Expand All @@ -21,20 +21,20 @@ langchain-openai = "^1.6"
langchain-text-splitters = "^1.1"
nltk = "^3.10.3"
pytest = "^9.1.1"
sentry-sdk = "^2.70.0"
sentry-sdk = "^2.71.0"
psycopg2-binary = "^2.9.13"
langfuse = "^4.15.4"
opentelemetry-instrumentation-anthropic = "^0.62.3"
opentelemetry-instrumentation-threading = "0.65b0"
pandas = "^2.2"
langfuse = "^4.15.6"
opentelemetry-instrumentation-anthropic = "^0.62.4"
opentelemetry-instrumentation-threading = "0.66b0"
pandas = "^3.0"
pgvector = "^0.5.0"

[tool.poetry.group.dev]
optional = false

[tool.poetry.group.dev.dependencies]
pytest = "^9.1.1"
ruff = "^0.16.8"
ruff = "^0.16.10"

[build-system]
requires = ["poetry-core"]
Expand Down
1 change: 0 additions & 1 deletion services/global_chat/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
router:
model: "claude-haiku"
max_tokens: 500
temperature: 0.0

# Planner configuration (complex orchestration)
planner:
Expand Down
12 changes: 6 additions & 6 deletions services/global_chat/planner.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from typing import List, Dict, Optional
from concurrent.futures import ThreadPoolExecutor, as_completed
from dataclasses import dataclass
import httpx
import httpx2
import anthropic
from anthropic import Anthropic, BadRequestError
import sentry_sdk
Expand Down Expand Up @@ -76,7 +76,7 @@ def _api_error_message(error: Exception) -> str:
if isinstance(message, str) and message:
return message

if isinstance(error, (anthropic.APIConnectionError, httpx.RequestError)):
if isinstance(error, (anthropic.APIConnectionError, httpx2.RequestError)):
return str(error) or type(error).__name__

status = getattr(error, "status_code", None)
Expand Down Expand Up @@ -160,7 +160,7 @@ def _model_api_failure(error: Exception) -> tuple[int, str, str, dict]:
# leaves the server and nothing upstream is trusted to be key-free.
details = {"upstream_message": mask_secrets(upstream)}

if isinstance(error, (anthropic.APIConnectionError, httpx.RequestError)):
if isinstance(error, (anthropic.APIConnectionError, httpx2.RequestError)):
cause = getattr(error, "__cause__", None)
if cause:
# A proxy error carries the proxy URL, credentials and all.
Expand Down Expand Up @@ -437,10 +437,10 @@ def run(

except ApolloError:
raise
except (anthropic.APIError, httpx.RequestError) as e:
except (anthropic.APIError, httpx2.RequestError) as e:
# A rejection from the model API is not a tool failure.
#
# httpx.RequestError as well as APIError: the SDK only
# httpx2.RequestError as well as APIError: the SDK only
# wraps httpx exceptions around the initial send, and on a
# streaming request that returns once the headers arrive.
# The body is read later, unwrapped, so a stream that dies
Expand Down Expand Up @@ -642,7 +642,7 @@ def _call_api(self, system_prompt, messages, stream, stream_manager, tool_choice
# Per-request timeout (same values as the SDK default):
# required for non-streaming calls with max_tokens > ~21k,
# which the SDK otherwise rejects.
timeout=httpx.Timeout(600.0, connect=5.0),
timeout=httpx2.Timeout(600.0, connect=5.0),
**_CONTEXT_MANAGEMENT,
)
return response
Expand Down
2 changes: 0 additions & 2 deletions services/global_chat/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,6 @@ def __init__(self, config_loader: ConfigLoader, api_key: Optional[str] = None):
router_config = config_loader.config.get("router", {})
self.model = resolve_model(router_config.get("model", "claude-haiku"))
self.max_tokens = router_config.get("max_tokens", 500)
self.temperature = router_config.get("temperature", 0.0)

logger.info(f"RouterAgent initialized with model: {self.model}")

Expand Down Expand Up @@ -181,7 +180,6 @@ def _make_routing_decision(
response = self.client.messages.create(
model=self.model,
max_tokens=self.max_tokens,
temperature=self.temperature,
system=[{"type": "text", "text": system_prompt}],
messages=[
{"role": "user", "content": routing_message}
Expand Down
70 changes: 35 additions & 35 deletions services/global_chat/tests/unit/test_planner.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from unittest.mock import patch

import anthropic
import httpx
import httpx2
import pytest
import yaml
from anthropic import BadRequestError
Expand Down Expand Up @@ -571,8 +571,8 @@ def test_model_api_error_is_not_reported_as_a_tool_failure() -> None:

# Shaped like a real rejection: the SDK's own str() is the dict repr,
# and the readable sentence is in the parsed body.
request = httpx.Request("POST", "https://api.anthropic.com/v1/messages")
response = httpx.Response(400, request=request, json={})
request = httpx2.Request("POST", "https://api.anthropic.com/v1/messages")
response = httpx2.Response(400, request=request, json={})
api_error = anthropic.BadRequestError(
"Error code: 400 - {'type': 'error', 'error': {...}}",
response=response,
Expand Down Expand Up @@ -629,11 +629,11 @@ def _raise_from_planner(error: Exception) -> ApolloError:
def test_a_rejected_key_does_not_describe_our_account_to_the_caller() -> None:
"""The upstream sentence for an auth failure is about our key, and every
user of the instance would be shown it in chat."""
request = httpx.Request("POST", "https://api.anthropic.com/v1/messages")
request = httpx2.Request("POST", "https://api.anthropic.com/v1/messages")
error = _raise_from_planner(
anthropic.AuthenticationError(
"Error code: 401 - {'type': 'error', 'error': {...}}",
response=httpx.Response(401, request=request, json={}),
response=httpx2.Response(401, request=request, json={}),
body={
"type": "error",
"error": {
Expand All @@ -656,11 +656,11 @@ def test_our_own_request_shape_being_wrong_is_not_blamed_on_the_caller() -> None
"""The planner sends beta headers, thinking and context-management config
and tool definitions the caller never touches. Anthropic rejects those
with a 400, and answering the caller 400 would blame them for it."""
request = httpx.Request("POST", "https://api.anthropic.com/v1/messages")
request = httpx2.Request("POST", "https://api.anthropic.com/v1/messages")
error = _raise_from_planner(
anthropic.BadRequestError(
"Error code: 400",
response=httpx.Response(400, request=request, json={}),
response=httpx2.Response(400, request=request, json={}),
body={
"type": "error",
"error": {
Expand All @@ -686,11 +686,11 @@ def test_our_own_request_shape_being_wrong_is_not_blamed_on_the_caller() -> None
def test_a_payload_over_the_size_limit_is_the_callers_to_act_on() -> None:
"""413 is the sibling of prompt-too-long: their workflow is too big, and a
retry of the same payload can never succeed."""
request = httpx.Request("POST", "https://api.anthropic.com/v1/messages")
request = httpx2.Request("POST", "https://api.anthropic.com/v1/messages")
error = _raise_from_planner(
anthropic.RequestTooLargeError(
"Error code: 413",
response=httpx.Response(413, request=request, json={}),
response=httpx2.Response(413, request=request, json={}),
body={"type": "error", "error": {"type": "request_too_large",
"message": "request body too large"}},
),
Expand All @@ -707,11 +707,11 @@ def test_a_payload_over_the_size_limit_is_the_callers_to_act_on() -> None:
def test_a_stream_that_dies_mid_reply_does_not_record_the_opening_200() -> None:
"""The SDK builds a mid-stream error from the response that opened the
stream, which succeeded, so its status would read as a call that worked."""
request = httpx.Request("POST", "https://api.anthropic.com/v1/messages")
request = httpx2.Request("POST", "https://api.anthropic.com/v1/messages")
error = _raise_from_planner(
anthropic.APIStatusError(
"Overloaded",
response=httpx.Response(200, request=request, json={}),
response=httpx2.Response(200, request=request, json={}),
body={"type": "error", "error": {"type": "overloaded_error",
"message": "Overloaded"}},
),
Expand All @@ -726,13 +726,13 @@ def test_a_mid_stream_failure_is_classified_by_what_the_body_says() -> None:
opened the stream, whatever actually went wrong, so the class says nothing
and the body's type is the only thing left. Global chat streams, so this
is the common path, not the corner."""
request = httpx.Request("POST", "https://api.anthropic.com/v1/messages")
request = httpx2.Request("POST", "https://api.anthropic.com/v1/messages")

def mid_stream(kind: str, message: str) -> ApolloError:
return _raise_from_planner(
anthropic.APIStatusError(
f"{{'type': 'error', 'error': {{'type': '{kind}'}}}}",
response=httpx.Response(200, request=request, json={}),
response=httpx2.Response(200, request=request, json={}),
body={"type": "error",
"error": {"type": kind, "message": message}},
),
Expand Down Expand Up @@ -768,11 +768,11 @@ def test_an_overload_with_no_readable_body_still_says_to_try_again() -> None:
"""A 529 is the response most likely to come back from an edge with an
HTML body, and then there is no type to read. It is also the one kind
whose advice differs, so falling back to the class matters here."""
request = httpx.Request("POST", "https://api.anthropic.com/v1/messages")
request = httpx2.Request("POST", "https://api.anthropic.com/v1/messages")
error = _raise_from_planner(
anthropic.OverloadedError(
"Error code: 529",
response=httpx.Response(529, request=request, text="<html>busy</html>"),
response=httpx2.Response(529, request=request, text="<html>busy</html>"),
body="<html>busy</html>",
),
)
Expand All @@ -784,11 +784,11 @@ def test_an_overload_with_no_readable_body_still_says_to_try_again() -> None:
def test_details_do_not_carry_key_shaped_values_off_the_server() -> None:
"""details is serialised to the caller (errors.ts toErrorPayload), so the
upstream sentence leaves the server even though only message is rendered."""
request = httpx.Request("POST", "https://api.anthropic.com/v1/messages")
request = httpx2.Request("POST", "https://api.anthropic.com/v1/messages")
error = _raise_from_planner(
anthropic.BadRequestError(
"Error code: 400",
response=httpx.Response(400, request=request, json={}),
response=httpx2.Response(400, request=request, json={}),
body={
"type": "error",
"error": {
Expand All @@ -805,8 +805,8 @@ def test_details_do_not_carry_key_shaped_values_off_the_server() -> None:
def test_the_cause_of_a_connection_failure_is_masked_too() -> None:
"""A proxy error carries the proxy URL, which is where credentials get
written, and details is serialised to the caller."""
request = httpx.Request("POST", "https://api.anthropic.com/v1/messages")
dropped = httpx.ProxyError("proxy rejected")
request = httpx2.Request("POST", "https://api.anthropic.com/v1/messages")
dropped = httpx2.ProxyError("proxy rejected")
dropped.__cause__ = Exception("http://user:sk-ant-api03-" + "b" * 95 + "@proxy:8080")

error = _raise_from_planner(dropped)
Expand All @@ -817,9 +817,9 @@ def test_the_cause_of_a_connection_failure_is_masked_too() -> None:
def test_a_dropped_connection_keeps_the_text_httpx_wrote() -> None:
"""The SDK only wraps httpx around the initial send, so a stream that dies
while the body is being read raises a raw httpx error with no status."""
request = httpx.Request("POST", "https://api.anthropic.com/v1/messages")
request = httpx2.Request("POST", "https://api.anthropic.com/v1/messages")
error = _raise_from_planner(
httpx.ReadError("peer closed connection", request=request),
httpx2.ReadError("peer closed connection", request=request),
)

assert error.code == 503
Expand Down Expand Up @@ -881,11 +881,11 @@ class FakeError(Exception):
def test_a_rate_limit_keeps_the_retry_after_the_bridge_contract_pins() -> None:
"""platform/test/server.test.ts asserts RATE_LIMIT carries retry_after, and
dropping it would leave a caller with nothing to wait on."""
request = httpx.Request("POST", "https://api.anthropic.com/v1/messages")
request = httpx2.Request("POST", "https://api.anthropic.com/v1/messages")
error = _raise_from_planner(
anthropic.RateLimitError(
"Error code: 429",
response=httpx.Response(
response=httpx2.Response(
429, request=request, json={}, headers={"retry-after": "30"},
),
body={"type": "error", "error": {"type": "rate_limit_error",
Expand All @@ -899,11 +899,11 @@ def test_a_rate_limit_keeps_the_retry_after_the_bridge_contract_pins() -> None:


def test_a_rate_limit_without_the_header_still_gives_something_to_wait_on() -> None:
request = httpx.Request("POST", "https://api.anthropic.com/v1/messages")
request = httpx2.Request("POST", "https://api.anthropic.com/v1/messages")
error = _raise_from_planner(
anthropic.RateLimitError(
"Error code: 429",
response=httpx.Response(429, request=request, json={}),
response=httpx2.Response(429, request=request, json={}),
body={},
),
)
Expand All @@ -916,13 +916,13 @@ def test_a_broken_retry_after_header_does_not_raise_inside_the_handler() -> None
header would raise from inside the except block and arrive as the generic
500 this whole change exists to remove. And the value is someone else's,
so a caller told to wait a century would wait forever."""
request = httpx.Request("POST", "https://api.anthropic.com/v1/messages")
request = httpx2.Request("POST", "https://api.anthropic.com/v1/messages")

def rate_limited(header: bytes) -> ApolloError:
return _raise_from_planner(
anthropic.RateLimitError(
"Error code: 429",
response=httpx.Response(
response=httpx2.Response(
429, request=request, json={},
headers=[(b"retry-after", header)],
),
Expand All @@ -947,17 +947,17 @@ def test_our_account_failing_is_not_reported_as_the_caller_failing() -> None:
"""A 403 from the model API means our org cannot use it. Passing it
through would answer the caller with a verdict we reached about
ourselves, which is what the 401 rule exists to prevent."""
request = httpx.Request("POST", "https://api.anthropic.com/v1/messages")
request = httpx2.Request("POST", "https://api.anthropic.com/v1/messages")
for sdk_error in (
anthropic.PermissionDeniedError(
"Error code: 403",
response=httpx.Response(403, request=request, json={}),
response=httpx2.Response(403, request=request, json={}),
body={"type": "error", "error": {"type": "permission_error",
"message": "not allowed"}},
),
anthropic.NotFoundError(
"Error code: 404",
response=httpx.Response(404, request=request, json={}),
response=httpx2.Response(404, request=request, json={}),
body={"type": "error", "error": {"type": "not_found_error",
"message": "model: nope"}},
),
Expand All @@ -974,7 +974,7 @@ def test_connection_failure_is_not_reported_as_a_tool_failure() -> None:
planner.model = "test-model"
planner.max_tool_calls = 5

request = httpx.Request("POST", "https://api.anthropic.com/v1/messages")
request = httpx2.Request("POST", "https://api.anthropic.com/v1/messages")
api_error = anthropic.APIConnectionError(request=request)

with (
Expand Down Expand Up @@ -1002,7 +1002,7 @@ def test_stream_dropping_midway_is_not_reported_as_a_tool_failure() -> None:
planner.model = "test-model"
planner.max_tool_calls = 5

dropped = httpx.RemoteProtocolError(
dropped = httpx2.RemoteProtocolError(
"peer closed connection without sending complete message body"
)

Expand All @@ -1023,7 +1023,7 @@ def test_connection_error_keeps_the_sdk_wording() -> None:
"""APIConnectionError's message is the SDK's own constant, not upstream
text, so it is worth more to a reader than the class name."""
error = anthropic.APIConnectionError(
request=httpx.Request("POST", "https://api.anthropic.com/v1/messages")
request=httpx2.Request("POST", "https://api.anthropic.com/v1/messages")
)

assert _api_error_message(error) == "Connection error."
Expand Down Expand Up @@ -1454,9 +1454,9 @@ def test_meta_omits_the_web_fields_when_web_search_is_off() -> None:


def make_bad_request(message: str = "web search is not enabled for this account") -> BadRequestError:
request = httpx.Request("POST", "https://api.anthropic.com/v1/messages")
request = httpx2.Request("POST", "https://api.anthropic.com/v1/messages")
body = {"type": "error", "error": {"type": "invalid_request_error", "message": message}}
return BadRequestError(message, response=httpx.Response(400, request=request, json=body), body=body)
return BadRequestError(message, response=httpx2.Response(400, request=request, json=body), body=body)


def test_a_bad_request_with_web_tools_retries_without_them() -> None:
Expand Down
Loading
Loading