heph 0.0.49__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ai/__init__.py +1 -0
- ai/diagnostics/__init__.py +77 -0
- ai/diagnostics/py.typed +1 -0
- ai/logging/__init__.py +216 -0
- ai/logging/py.typed +1 -0
- ai/providers/__init__.py +24 -0
- ai/providers/access.py +32 -0
- ai/providers/api_profiles.py +127 -0
- ai/providers/catalog.py +542 -0
- ai/providers/config.py +358 -0
- ai/providers/endpoints.py +21 -0
- ai/providers/keyring_store.py +131 -0
- ai/providers/llama_cpp.py +1464 -0
- ai/providers/model_choices.py +175 -0
- ai/providers/model_support.py +76 -0
- ai/providers/oauth.py +554 -0
- ai/providers/py.typed +1 -0
- ai/providers/reasoning.py +33 -0
- ai/providers/registry.py +441 -0
- ai/providers/volatile_keys.py +17 -0
- ai/py.typed +1 -0
- ai/runtime/__init__.py +73 -0
- ai/runtime/_api_types.py +40 -0
- ai/runtime/codex_backend.py +475 -0
- ai/runtime/config.py +71 -0
- ai/runtime/conversation.py +37 -0
- ai/runtime/delta.py +17 -0
- ai/runtime/engine.py +1188 -0
- ai/runtime/errors.py +56 -0
- ai/runtime/events.py +129 -0
- ai/runtime/messages.py +30 -0
- ai/runtime/prompt_cache.py +134 -0
- ai/runtime/py.typed +1 -0
- ai/runtime/request_payload.py +51 -0
- ai/runtime/resilience.py +131 -0
- ai/runtime/thinking.py +39 -0
- ai/runtime/tool_deltas.py +38 -0
- ai/runtime/usage.py +232 -0
- ai/runtime/usage_payload.py +64 -0
- ai/types/__init__.py +46 -0
- ai/types/py.typed +1 -0
- extensions/__init__.py +1 -0
- extensions/contracts.py +41 -0
- extensions/py.typed +1 -0
- heph/__init__.py +10 -0
- heph/cli/__init__.py +7 -0
- heph/cli/main.py +895 -0
- heph/cli/py.typed +1 -0
- heph/commands/__init__.py +145 -0
- heph/commands/_base.py +72 -0
- heph/commands/armory.py +166 -0
- heph/commands/auth.py +246 -0
- heph/commands/compact.py +36 -0
- heph/commands/display.py +195 -0
- heph/commands/help.py +81 -0
- heph/commands/local.py +222 -0
- heph/commands/memory.py +34 -0
- heph/commands/model.py +124 -0
- heph/commands/py.typed +1 -0
- heph/commands/session.py +327 -0
- heph/commands/settings.py +59 -0
- heph/commands/study.py +341 -0
- heph/commands/suggestions.py +12 -0
- heph/commands/terminal_text.py +14 -0
- heph/identity/README.md +6 -0
- heph/local_llm.py +29 -0
- heph/product/__init__.py +5 -0
- heph/product/context.py +5 -0
- heph/product/py.typed +1 -0
- heph/prompts/README.md +6 -0
- heph/py.typed +1 -0
- heph/release_state.py +130 -0
- heph/sdk/__init__.py +353 -0
- heph/sdk/capabilities.py +1397 -0
- heph/sdk/compatibility.py +449 -0
- heph/sdk/config.py +170 -0
- heph/sdk/events.py +351 -0
- heph/sdk/factory.py +198 -0
- heph/sdk/materials.py +123 -0
- heph/sdk/method_validation.py +1175 -0
- heph/sdk/methods.py +1610 -0
- heph/sdk/models.py +90 -0
- heph/sdk/operation_stream.py +64 -0
- heph/sdk/providers.py +165 -0
- heph/sdk/runtime.py +737 -0
- heph/sdk/service.py +676 -0
- heph/sdk/service_availability.py +237 -0
- heph/sdk/service_contract.py +150 -0
- heph/sdk/service_routes.py +445 -0
- heph/sdk/settings.py +257 -0
- heph/sdk/state.py +398 -0
- heph/sdk/stdio.py +522 -0
- heph/sdk/stdio_client.py +1403 -0
- heph/sdk/stdio_contract.py +206 -0
- heph/sdk/stdio_json.py +27 -0
- heph/sdk/stdio_requests.py +122 -0
- heph/sdk/stdio_routes.py +97 -0
- heph/sdk/stdio_state.py +181 -0
- heph/sdk/value_types.py +181 -0
- heph/state/README.md +10 -0
- heph/state/release.toml +13 -0
- heph-0.0.49.dist-info/METADATA +95 -0
- heph-0.0.49.dist-info/RECORD +348 -0
- heph-0.0.49.dist-info/WHEEL +5 -0
- heph-0.0.49.dist-info/entry_points.txt +3 -0
- heph-0.0.49.dist-info/licenses/LICENSE +21 -0
- heph-0.0.49.dist-info/top_level.txt +5 -0
- hephaion/__init__.py +1 -0
- hephaion/_types/__init__.py +19 -0
- hephaion/_types/py.typed +1 -0
- hephaion/agent/__init__.py +32 -0
- hephaion/agent/armory_tools.py +441 -0
- hephaion/agent/citation.py +164 -0
- hephaion/agent/compact.py +365 -0
- hephaion/agent/dispatch.py +467 -0
- hephaion/agent/dispatch_support.py +294 -0
- hephaion/agent/file_tools.py +375 -0
- hephaion/agent/material_tools.py +181 -0
- hephaion/agent/model_stream.py +269 -0
- hephaion/agent/mutation_queue.py +97 -0
- hephaion/agent/path_safety.py +73 -0
- hephaion/agent/prompt.py +273 -0
- hephaion/agent/py.typed +1 -0
- hephaion/agent/runtime_notes.py +156 -0
- hephaion/agent/shell_tools.py +239 -0
- hephaion/agent/steering.py +42 -0
- hephaion/agent/tool_execution.py +513 -0
- hephaion/agent/tool_registry.py +129 -0
- hephaion/agent/tool_schema.py +52 -0
- hephaion/agent/tools.py +411 -0
- hephaion/agent/web_tools.py +339 -0
- hephaion/armory/__init__.py +37 -0
- hephaion/armory/cli.py +105 -0
- hephaion/armory/py.typed +1 -0
- hephaion/armory/search.py +255 -0
- hephaion/armory/state_files.py +218 -0
- hephaion/armory/storage.py +130 -0
- hephaion/armory/trust.py +35 -0
- hephaion/chat/__init__.py +0 -0
- hephaion/chat/agent_request.py +130 -0
- hephaion/chat/armory_turn.py +222 -0
- hephaion/chat/automation.py +37 -0
- hephaion/chat/citation_patterns.py +22 -0
- hephaion/chat/cli.py +51 -0
- hephaion/chat/compaction.py +56 -0
- hephaion/chat/conversation_context.py +70 -0
- hephaion/chat/current_topic_planning.py +151 -0
- hephaion/chat/events.py +61 -0
- hephaion/chat/evidence.py +1012 -0
- hephaion/chat/evidence_assessment.py +508 -0
- hephaion/chat/evidence_format.py +21 -0
- hephaion/chat/evidence_notices.py +120 -0
- hephaion/chat/evidence_overview.py +172 -0
- hephaion/chat/evidence_prompt.py +43 -0
- hephaion/chat/evidence_text.py +100 -0
- hephaion/chat/followup_intent_resolution.py +332 -0
- hephaion/chat/followup_retrieval.py +706 -0
- hephaion/chat/intent.py +143 -0
- hephaion/chat/intent_resolution.py +373 -0
- hephaion/chat/learning_reply.py +440 -0
- hephaion/chat/learning_signals.py +256 -0
- hephaion/chat/material_state.py +359 -0
- hephaion/chat/message_delivery.py +84 -0
- hephaion/chat/model_selection.py +86 -0
- hephaion/chat/model_text.py +55 -0
- hephaion/chat/orchestrator.py +46 -0
- hephaion/chat/overview_cues.py +122 -0
- hephaion/chat/overview_planning.py +89 -0
- hephaion/chat/overview_reply.py +761 -0
- hephaion/chat/overview_tables.py +121 -0
- hephaion/chat/overview_topics.py +162 -0
- hephaion/chat/overview_validation.py +295 -0
- hephaion/chat/prior_answer.py +744 -0
- hephaion/chat/priority_planning.py +63 -0
- hephaion/chat/provider_selection.py +22 -0
- hephaion/chat/py.typed +1 -0
- hephaion/chat/replayability_planning.py +50 -0
- hephaion/chat/reply_evidence.py +58 -0
- hephaion/chat/reply_repair.py +549 -0
- hephaion/chat/reply_text.py +232 -0
- hephaion/chat/session.py +553 -0
- hephaion/chat/session_persistence.py +78 -0
- hephaion/chat/storage.py +224 -0
- hephaion/chat/titles.py +49 -0
- hephaion/chat/turn_contract.py +282 -0
- hephaion/chat/turn_contract_checks.py +95 -0
- hephaion/chat/turn_event_helpers.py +37 -0
- hephaion/chat/turn_execution.py +700 -0
- hephaion/chat/turn_finalization.py +945 -0
- hephaion/chat/turn_history.py +160 -0
- hephaion/chat/turn_lifecycle.py +319 -0
- hephaion/chat/turn_orchestrator.py +36 -0
- hephaion/chat/turn_outputs.py +61 -0
- hephaion/chat/turn_planning.py +1020 -0
- hephaion/chat/turn_predicates.py +86 -0
- hephaion/chat/turn_query.py +257 -0
- hephaion/chat/usage.py +146 -0
- hephaion/diagnostics/__init__.py +0 -0
- hephaion/diagnostics/crashes.py +328 -0
- hephaion/diagnostics/events.py +143 -0
- hephaion/diagnostics/py.typed +1 -0
- hephaion/diagnostics/traces.py +102 -0
- hephaion/learning/__init__.py +28 -0
- hephaion/learning/actions.py +40 -0
- hephaion/learning/automation.py +254 -0
- hephaion/learning/cli.py +196 -0
- hephaion/learning/constellation.py +231 -0
- hephaion/learning/environment.py +131 -0
- hephaion/learning/fixtures/__init__.py +3 -0
- hephaion/learning/observation.py +392 -0
- hephaion/learning/observation_audit.py +257 -0
- hephaion/learning/policy.py +90 -0
- hephaion/learning/policy_artifact.py +225 -0
- hephaion/learning/puffer_backend.py +595 -0
- hephaion/learning/reward.py +430 -0
- hephaion/learning/storage.py +437 -0
- hephaion/learning/training.py +726 -0
- hephaion/matching/__init__.py +103 -0
- hephaion/matching/py.typed +1 -0
- hephaion/materials/__init__.py +426 -0
- hephaion/materials/cli.py +97 -0
- hephaion/materials/importing.py +168 -0
- hephaion/materials/py.typed +1 -0
- hephaion/memory/__init__.py +488 -0
- hephaion/memory/extract.py +197 -0
- hephaion/memory/py.typed +1 -0
- hephaion/memory/workflow.py +48 -0
- hephaion/parameters/__init__.py +0 -0
- hephaion/parameters/cli.py +284 -0
- hephaion/parameters/default.toml +14 -0
- hephaion/parameters/py.typed +1 -0
- hephaion/parameters/settings.py +337 -0
- hephaion/privacy/__init__.py +0 -0
- hephaion/privacy/consent.py +215 -0
- hephaion/privacy/py.typed +1 -0
- hephaion/privacy/release.py +9 -0
- hephaion/py.typed +1 -0
- hephaion/rag/__init__.py +111 -0
- hephaion/rag/chunker.py +1191 -0
- hephaion/rag/config.py +8 -0
- hephaion/rag/context.py +263 -0
- hephaion/rag/health.py +80 -0
- hephaion/rag/hybrid.py +275 -0
- hephaion/rag/index.py +1152 -0
- hephaion/rag/index_state.py +36 -0
- hephaion/rag/index_timeout.py +220 -0
- hephaion/rag/optional_backends.py +168 -0
- hephaion/rag/py.typed +1 -0
- hephaion/rag/query_audit.py +99 -0
- hephaion/rag/query_transform.py +398 -0
- hephaion/rag/retrieval_types.py +69 -0
- hephaion/rag/retrieve.py +832 -0
- hephaion/rag/retrieve_compound.py +149 -0
- hephaion/rag/scoring.py +251 -0
- hephaion/rag/semantic.py +164 -0
- hephaion/rag/source_mapping.py +123 -0
- hephaion/rag/sparse.py +497 -0
- hephaion/rag/vector.py +69 -0
- hephaion/safety/__init__.py +36 -0
- hephaion/safety/contracts.py +78 -0
- hephaion/safety/local.py +41 -0
- hephaion/safety/py.typed +1 -0
- hephaion/study/__init__.py +135 -0
- hephaion/study/artifacts.py +679 -0
- hephaion/study/assessment.py +107 -0
- hephaion/study/controller.py +842 -0
- hephaion/study/exam.py +387 -0
- hephaion/study/exam_bank.py +218 -0
- hephaion/study/intent.py +1 -0
- hephaion/study/knowledge.py +736 -0
- hephaion/study/mastery.py +23 -0
- hephaion/study/policy.py +882 -0
- hephaion/study/priority.py +823 -0
- hephaion/study/priority_analysis.py +92 -0
- hephaion/study/priority_progress.py +77 -0
- hephaion/study/priority_rendering.py +549 -0
- hephaion/study/priority_report.py +731 -0
- hephaion/study/priority_topics.py +24 -0
- hephaion/study/priority_types.py +172 -0
- hephaion/study/priority_web.py +116 -0
- hephaion/study/prompt_plans.py +846 -0
- hephaion/study/py.typed +1 -0
- hephaion/study/schedule.py +744 -0
- hephaion/study/state.py +208 -0
- hephaion/version/__init__.py +5 -0
- hephaion/version/py.typed +1 -0
- hephaion/vocab/__init__.py +18 -0
- hephaion/vocab/drill.py +261 -0
- hephaion/vocab/parser.py +209 -0
- hephaion/vocab/py.typed +1 -0
- hephaion/vocab/scheduler.py +102 -0
- hephaion/vocab/state.py +245 -0
- interfaces/__init__.py +1 -0
- interfaces/palette/__init__.py +94 -0
- interfaces/palette/py.typed +1 -0
- interfaces/py.typed +1 -0
- interfaces/terminal/__init__.py +255 -0
- interfaces/terminal/history.py +65 -0
- interfaces/terminal/input.py +100 -0
- interfaces/terminal/py.typed +1 -0
- interfaces/terminal/source_open.py +60 -0
- interfaces/terminal/theme_state.py +29 -0
- interfaces/tui/__init__.py +382 -0
- interfaces/tui/app_actions.py +622 -0
- interfaces/tui/armory.py +841 -0
- interfaces/tui/armory_browser.py +377 -0
- interfaces/tui/auth_flows.py +318 -0
- interfaces/tui/cell_text.py +39 -0
- interfaces/tui/command_access.py +54 -0
- interfaces/tui/command_output.py +63 -0
- interfaces/tui/composer_controls.py +918 -0
- interfaces/tui/dependencies.py +21 -0
- interfaces/tui/display_text.py +558 -0
- interfaces/tui/external_commands.py +320 -0
- interfaces/tui/flow_state.py +19 -0
- interfaces/tui/history.py +54 -0
- interfaces/tui/ids.py +25 -0
- interfaces/tui/inline_flows.py +1764 -0
- interfaces/tui/inline_menu.py +819 -0
- interfaces/tui/keybinds.py +109 -0
- interfaces/tui/keyboard_protocol.py +111 -0
- interfaces/tui/keymap.py +528 -0
- interfaces/tui/local_flows.py +584 -0
- interfaces/tui/materials.py +561 -0
- interfaces/tui/model_flow.py +62 -0
- interfaces/tui/model_flows.py +156 -0
- interfaces/tui/option_list_layout.py +64 -0
- interfaces/tui/py.typed +1 -0
- interfaces/tui/render_state.py +37 -0
- interfaces/tui/resize.py +517 -0
- interfaces/tui/rich_transcript.py +380 -0
- interfaces/tui/routing.py +128 -0
- interfaces/tui/search_screen.py +320 -0
- interfaces/tui/session_actions.py +211 -0
- interfaces/tui/session_flows.py +381 -0
- interfaces/tui/session_state.py +90 -0
- interfaces/tui/shortcut_hints.py +27 -0
- interfaces/tui/slash_command.py +82 -0
- interfaces/tui/slash_completion.py +311 -0
- interfaces/tui/startup_discovery.py +48 -0
- interfaces/tui/status.py +199 -0
- interfaces/tui/streaming.py +640 -0
- interfaces/tui/style.py +446 -0
- interfaces/tui/textual_compat.py +59 -0
- interfaces/tui/transcript.py +807 -0
- interfaces/tui/transparent.py +454 -0
- interfaces/tui/turns.py +223 -0
- interfaces/tui/widgets.py +430 -0
ai/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Self
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class NoopSpan:
|
|
7
|
+
__slots__ = ()
|
|
8
|
+
|
|
9
|
+
def set_attribute(self, key: str, value: object) -> object:
|
|
10
|
+
del key, value
|
|
11
|
+
return self
|
|
12
|
+
|
|
13
|
+
def end(self, end_time: float | None = None) -> None:
|
|
14
|
+
del end_time
|
|
15
|
+
|
|
16
|
+
def __enter__(self) -> Self:
|
|
17
|
+
return self
|
|
18
|
+
|
|
19
|
+
def __exit__(self, *args: object) -> None:
|
|
20
|
+
self.end()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class NoopTracer:
|
|
24
|
+
__slots__ = ()
|
|
25
|
+
|
|
26
|
+
def start_span(self, name: str, **kwargs: object) -> NoopSpan:
|
|
27
|
+
del name, kwargs
|
|
28
|
+
return NoopSpan()
|
|
29
|
+
|
|
30
|
+
def start_as_current_span(self, name: str, **kwargs: object) -> NoopSpan:
|
|
31
|
+
del name, kwargs
|
|
32
|
+
return NoopSpan()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class NoopInstrument:
|
|
36
|
+
__slots__ = ()
|
|
37
|
+
|
|
38
|
+
def record(self, value: float, _attributes: dict[str, str] | None = None) -> None:
|
|
39
|
+
del value, _attributes
|
|
40
|
+
|
|
41
|
+
def add(self, value: float, _attributes: dict[str, str] | None = None) -> None:
|
|
42
|
+
del value, _attributes
|
|
43
|
+
|
|
44
|
+
def set(self, value: float, _attributes: dict[str, str] | None = None) -> None:
|
|
45
|
+
del value, _attributes
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class NoopMeter:
|
|
49
|
+
__slots__ = ()
|
|
50
|
+
|
|
51
|
+
def create_histogram(self, name: str, **kwargs: object) -> NoopInstrument:
|
|
52
|
+
del name, kwargs
|
|
53
|
+
return NoopInstrument()
|
|
54
|
+
|
|
55
|
+
def create_counter(self, name: str, **kwargs: object) -> NoopInstrument:
|
|
56
|
+
del name, kwargs
|
|
57
|
+
return NoopInstrument()
|
|
58
|
+
|
|
59
|
+
create_up_down_counter = create_counter
|
|
60
|
+
|
|
61
|
+
def create_gauge(self, name: str, **kwargs: object) -> NoopInstrument:
|
|
62
|
+
del name, kwargs
|
|
63
|
+
return NoopInstrument()
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
_NOOP_TRACER = NoopTracer()
|
|
67
|
+
_NOOP_METER = NoopMeter()
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def get_tracer(name: str) -> NoopTracer:
|
|
71
|
+
del name
|
|
72
|
+
return _NOOP_TRACER
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def get_meter(name: str) -> NoopMeter:
|
|
76
|
+
del name
|
|
77
|
+
return _NOOP_METER
|
ai/diagnostics/py.typed
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
ai/logging/__init__.py
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
"""Structured logging, redaction, and timers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
import os
|
|
8
|
+
import re as _re
|
|
9
|
+
import sys
|
|
10
|
+
import time
|
|
11
|
+
from collections.abc import Mapping
|
|
12
|
+
from datetime import UTC, datetime
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import ClassVar, Self
|
|
15
|
+
|
|
16
|
+
from ai.types import is_object_list, is_string_mapping
|
|
17
|
+
|
|
18
|
+
_LOG_TEXT_MUTED = "#6F6F6F"
|
|
19
|
+
_LOG_ACCENT = "#D06A4A"
|
|
20
|
+
_LOG_ERROR = "#FF6B5A"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _ansi_fg(hex_color: str) -> str:
|
|
24
|
+
color = hex_color.lstrip("#")
|
|
25
|
+
red = int(color[0:2], 16)
|
|
26
|
+
green = int(color[2:4], 16)
|
|
27
|
+
blue = int(color[4:6], 16)
|
|
28
|
+
return f"\033[38;2;{red};{green};{blue}m"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# -- Redaction / scrubbing ---------------------------------------------------
|
|
32
|
+
|
|
33
|
+
# Patterns for dict keys whose values should always be redacted
|
|
34
|
+
_SENSITIVE_KEY_PATTERNS: list[_re.Pattern[str]] = [
|
|
35
|
+
_re.compile(r"(?i)(api.?key|secret|token(?!s)|password|auth(orization|entication))"),
|
|
36
|
+
_re.compile(r"(?i)(bearer|credential|private.?key)"),
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
# Unanchored versions — find secrets embedded within longer text
|
|
40
|
+
_SENSITIVE_TEXT_PATTERNS: list[_re.Pattern[str]] = [
|
|
41
|
+
_re.compile(r"sk-or-v1-[a-zA-Z0-9\-_]{20,}"), # OpenRouter API keys
|
|
42
|
+
_re.compile(r"sk-proj-[a-zA-Z0-9\-_]{20,}"), # OpenAI project API keys
|
|
43
|
+
_re.compile(r"sk-[a-zA-Z0-9]{20,}"), # OpenAI-style API keys
|
|
44
|
+
_re.compile(r"sk-ant-[a-zA-Z0-9\-]{20,}"), # Provider API keys
|
|
45
|
+
_re.compile(r"\b(?:AKIA|ASIA)[0-9A-Z]{16}\b"), # AWS access keys
|
|
46
|
+
_re.compile(r"AIza[0-9A-Za-z\-_]{35}"), # Google API keys
|
|
47
|
+
_re.compile(r"ya29\.[0-9A-Za-z\-_]+"), # Google OAuth tokens
|
|
48
|
+
_re.compile(r"Bearer\s+\S+", _re.IGNORECASE), # Bearer tokens
|
|
49
|
+
_re.compile(r"\b[a-f0-9]{32,}\b"), # Long hex strings (potential tokens)
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
_REDACTED = "***REDACTED***"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def redact_text(text: str) -> str:
|
|
56
|
+
if not text:
|
|
57
|
+
return text
|
|
58
|
+
for pattern in _SENSITIVE_TEXT_PATTERNS:
|
|
59
|
+
text = pattern.sub(_REDACTED, text)
|
|
60
|
+
return text
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _redact_dict(data: Mapping[str, object]) -> dict[str, object]:
|
|
64
|
+
return {key: _redact_value(key, value) for key, value in data.items()}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def redact_mapping(data: Mapping[str, object]) -> dict[str, object]:
|
|
68
|
+
return _redact_dict(data)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _redact_value(key: str, value: object) -> object:
|
|
72
|
+
if any(pattern.search(key) for pattern in _SENSITIVE_KEY_PATTERNS):
|
|
73
|
+
return _REDACTED
|
|
74
|
+
return _redact_unkeyed_value(value)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _redact_unkeyed_value(value: object) -> object:
|
|
78
|
+
if is_string_mapping(value):
|
|
79
|
+
return _redact_dict(value)
|
|
80
|
+
if is_object_list(value):
|
|
81
|
+
return [_redact_unkeyed_value(item) for item in value]
|
|
82
|
+
if isinstance(value, str):
|
|
83
|
+
return redact_text(value)
|
|
84
|
+
return value
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class _JsonFormatter(logging.Formatter):
|
|
88
|
+
def format(self, record: logging.LogRecord) -> str:
|
|
89
|
+
entry: dict[str, object] = {
|
|
90
|
+
"ts": datetime.fromtimestamp(record.created, tz=UTC).isoformat(),
|
|
91
|
+
"level": record.levelname,
|
|
92
|
+
"logger": record.name,
|
|
93
|
+
"msg": record.getMessage(),
|
|
94
|
+
}
|
|
95
|
+
fields = getattr(record, "fields", None)
|
|
96
|
+
if is_string_mapping(fields):
|
|
97
|
+
entry.update(fields)
|
|
98
|
+
if record.exc_info and record.exc_info[1] is not None:
|
|
99
|
+
entry["exc"] = self.formatException(record.exc_info)
|
|
100
|
+
return json.dumps(_redact_dict(entry), default=str, ensure_ascii=False)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class _TextFormatter(logging.Formatter):
|
|
104
|
+
_LEVEL_COLOURS: ClassVar[dict[str, str]] = {
|
|
105
|
+
"DEBUG": _ansi_fg(_LOG_TEXT_MUTED),
|
|
106
|
+
"INFO": _ansi_fg(_LOG_ACCENT),
|
|
107
|
+
"WARNING": _ansi_fg(_LOG_ACCENT),
|
|
108
|
+
"ERROR": _ansi_fg(_LOG_ERROR),
|
|
109
|
+
"CRITICAL": f"\033[1m{_ansi_fg(_LOG_ERROR)}",
|
|
110
|
+
}
|
|
111
|
+
_RESET = "\033[0m"
|
|
112
|
+
_DIM = f"\033[2m{_ansi_fg(_LOG_TEXT_MUTED)}"
|
|
113
|
+
|
|
114
|
+
def format(self, record: logging.LogRecord) -> str:
|
|
115
|
+
ts = datetime.fromtimestamp(record.created, tz=UTC).strftime("%H:%M:%S")
|
|
116
|
+
colour = self._LEVEL_COLOURS.get(record.levelname, "")
|
|
117
|
+
level = f"{colour}{record.levelname:<8}{self._RESET}"
|
|
118
|
+
parts = [f"{self._DIM}{ts}{self._RESET} {level} {record.name}: {record.getMessage()}"]
|
|
119
|
+
parts.extend(self._field_lines(record))
|
|
120
|
+
parts.extend(self._exception_lines(record))
|
|
121
|
+
return "\n".join(parts)
|
|
122
|
+
|
|
123
|
+
def _field_lines(self, record: logging.LogRecord) -> list[str]:
|
|
124
|
+
fields = getattr(record, "fields", None)
|
|
125
|
+
if not is_string_mapping(fields):
|
|
126
|
+
return []
|
|
127
|
+
return [
|
|
128
|
+
f" {self._DIM}{key}={value}{self._RESET}"
|
|
129
|
+
for key, value in _redact_dict(fields).items()
|
|
130
|
+
]
|
|
131
|
+
|
|
132
|
+
def _exception_lines(self, record: logging.LogRecord) -> list[str]:
|
|
133
|
+
if record.exc_info and record.exc_info[1] is not None:
|
|
134
|
+
return [self.formatException(record.exc_info)]
|
|
135
|
+
return []
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
LOG_LEVEL_ENV = "HEPHAION_LOG_LEVEL"
|
|
139
|
+
LOG_FILE_ENV = "HEPHAION_LOG_FILE"
|
|
140
|
+
LOG_FORMAT_ENV = "HEPHAION_LOG_FORMAT"
|
|
141
|
+
|
|
142
|
+
_hephaion_logger_initialised = False
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def get_logger(name: str) -> logging.Logger:
|
|
146
|
+
_ensure_hephaion_logger()
|
|
147
|
+
if not name.startswith("hephaion"):
|
|
148
|
+
name = f"hephaion.{name}"
|
|
149
|
+
return logging.getLogger(name)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _ensure_hephaion_logger() -> None:
|
|
153
|
+
global _hephaion_logger_initialised # noqa: PLW0603
|
|
154
|
+
if _hephaion_logger_initialised:
|
|
155
|
+
return
|
|
156
|
+
_hephaion_logger_initialised = True
|
|
157
|
+
|
|
158
|
+
level, fmt = _logging_config()
|
|
159
|
+
logger = logging.getLogger("hephaion")
|
|
160
|
+
logger.setLevel(level)
|
|
161
|
+
logger.propagate = False
|
|
162
|
+
if not logger.handlers:
|
|
163
|
+
logger.addHandler(_stderr_handler(level, fmt))
|
|
164
|
+
if log_file := os.environ.get(LOG_FILE_ENV):
|
|
165
|
+
logger.addHandler(_file_handler(Path(log_file), level))
|
|
166
|
+
_quiet_noisy_loggers()
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _logging_config() -> tuple[int, str]:
|
|
170
|
+
is_tty = sys.stderr.isatty()
|
|
171
|
+
default_level_name = "ERROR" if is_tty else "WARNING"
|
|
172
|
+
default_format = "text" if is_tty else "json"
|
|
173
|
+
level_name = os.environ.get(LOG_LEVEL_ENV, default_level_name).upper()
|
|
174
|
+
return getattr(logging, level_name, logging.WARNING), os.environ.get(
|
|
175
|
+
LOG_FORMAT_ENV,
|
|
176
|
+
default_format,
|
|
177
|
+
).lower()
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _stderr_handler(level: int, fmt: str) -> logging.Handler:
|
|
181
|
+
handler = logging.StreamHandler(sys.stderr)
|
|
182
|
+
handler.setLevel(level)
|
|
183
|
+
handler.setFormatter(_JsonFormatter() if fmt == "json" else _TextFormatter())
|
|
184
|
+
return handler
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _file_handler(path: Path, level: int) -> logging.Handler:
|
|
188
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
189
|
+
handler = logging.FileHandler(path, encoding="utf-8")
|
|
190
|
+
handler.setLevel(level)
|
|
191
|
+
handler.setFormatter(_JsonFormatter())
|
|
192
|
+
return handler
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _quiet_noisy_loggers() -> None:
|
|
196
|
+
for noisy in ("openai", "httpx", "httpcore", "urllib3", "sentence_transformers"):
|
|
197
|
+
logging.getLogger(noisy).setLevel(logging.WARNING)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
class Timer:
|
|
201
|
+
__slots__ = ("_end", "_start")
|
|
202
|
+
|
|
203
|
+
def __init__(self) -> None:
|
|
204
|
+
self._start: float = 0.0
|
|
205
|
+
self._end: float = 0.0
|
|
206
|
+
|
|
207
|
+
def __enter__(self) -> Self:
|
|
208
|
+
self._start = time.perf_counter()
|
|
209
|
+
return self
|
|
210
|
+
|
|
211
|
+
def __exit__(self, *args: object) -> None:
|
|
212
|
+
self._end = time.perf_counter()
|
|
213
|
+
|
|
214
|
+
@property
|
|
215
|
+
def ms(self) -> float:
|
|
216
|
+
return (self._end - self._start) * 1000
|
ai/logging/py.typed
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
ai/providers/__init__.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Multi-provider LLM configuration."""
|
|
2
|
+
|
|
3
|
+
from ai.providers.catalog import (
|
|
4
|
+
LiveProviderCatalog,
|
|
5
|
+
hydrate_provider_models,
|
|
6
|
+
prefetch_provider_model_catalogs,
|
|
7
|
+
)
|
|
8
|
+
from ai.providers.config import Provider, ProviderConfig, default_config, providers_dir
|
|
9
|
+
from ai.providers.keyring_store import mask_key, resolve_key
|
|
10
|
+
from ai.providers.registry import ModelInfo, get_registry
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"LiveProviderCatalog",
|
|
14
|
+
"ModelInfo",
|
|
15
|
+
"Provider",
|
|
16
|
+
"ProviderConfig",
|
|
17
|
+
"default_config",
|
|
18
|
+
"get_registry",
|
|
19
|
+
"hydrate_provider_models",
|
|
20
|
+
"mask_key",
|
|
21
|
+
"prefetch_provider_model_catalogs",
|
|
22
|
+
"providers_dir",
|
|
23
|
+
"resolve_key",
|
|
24
|
+
]
|
ai/providers/access.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Provider access decisions independent of UI adapters."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from ai.providers.catalog import hydrate_provider_models
|
|
6
|
+
from ai.providers.config import Provider, ProviderConfig
|
|
7
|
+
from ai.providers.endpoints import provider_uses_keyless_access
|
|
8
|
+
from ai.providers.keyring_store import resolve_key
|
|
9
|
+
from ai.providers.oauth import resolve_oauth_key
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def provider_is_accessible(provider: Provider, *, refresh_oauth: bool = True) -> bool:
|
|
13
|
+
if provider.slug == "openai-codex":
|
|
14
|
+
return bool(resolve_oauth_key(provider.slug, refresh_expired=refresh_oauth))
|
|
15
|
+
if provider_uses_keyless_access(provider.slug, provider.endpoint):
|
|
16
|
+
return True
|
|
17
|
+
return bool(
|
|
18
|
+
resolve_key(
|
|
19
|
+
provider.slug,
|
|
20
|
+
provider.api_key_env,
|
|
21
|
+
refresh_oauth=refresh_oauth,
|
|
22
|
+
)
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def activate_provider_config(pc: ProviderConfig, slug: str) -> Provider:
|
|
27
|
+
pc.set_active(slug)
|
|
28
|
+
hydrate_provider_models(pc, provider_slugs={slug})
|
|
29
|
+
provider = pc.providers[slug]
|
|
30
|
+
if not provider.current_model and provider.models:
|
|
31
|
+
provider.current_model = provider.models[0]
|
|
32
|
+
return provider
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""Provider-specific request profiles for OpenAI-compatible API surfaces."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Iterable
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import Literal, Protocol
|
|
8
|
+
|
|
9
|
+
from ai.providers.llama_cpp import LLAMA_CPP_PROVIDER_SLUG
|
|
10
|
+
|
|
11
|
+
ReasoningPayload = Literal["openai", "deepseek", "openrouter", "none"]
|
|
12
|
+
|
|
13
|
+
_REASONING_ORDER = ("low", "medium", "high", "xhigh")
|
|
14
|
+
_OPENAI_ENDPOINT = "https://api.openai.com/v1"
|
|
15
|
+
_DEEPSEEK_ENDPOINTS = ("https://api.deepseek.com", "https://api.deepseek.com/v1")
|
|
16
|
+
_OPENROUTER_ENDPOINT = "https://openrouter.ai/api/v1"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ProviderProfileConfig(Protocol):
|
|
20
|
+
provider_slug: str
|
|
21
|
+
base_url: str
|
|
22
|
+
model: str
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ReasoningPayloadConfig(ProviderProfileConfig, Protocol):
|
|
26
|
+
reasoning_level: str
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass(frozen=True, slots=True)
|
|
30
|
+
class ProviderRequestProfile:
|
|
31
|
+
name: str
|
|
32
|
+
reasoning_payload: ReasoningPayload
|
|
33
|
+
suppress_temperature_when_reasoning: bool = False
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
_GENERIC_OPENAI_COMPATIBLE_PROFILE = ProviderRequestProfile(
|
|
37
|
+
name="openai-compatible",
|
|
38
|
+
reasoning_payload="openai",
|
|
39
|
+
)
|
|
40
|
+
_DEEPSEEK_PROFILE = ProviderRequestProfile(
|
|
41
|
+
name="deepseek",
|
|
42
|
+
reasoning_payload="deepseek",
|
|
43
|
+
suppress_temperature_when_reasoning=True,
|
|
44
|
+
)
|
|
45
|
+
_OPENROUTER_PROFILE = ProviderRequestProfile(
|
|
46
|
+
name="openrouter",
|
|
47
|
+
reasoning_payload="openrouter",
|
|
48
|
+
)
|
|
49
|
+
_NO_REASONING_PROFILE = ProviderRequestProfile(
|
|
50
|
+
name="provider-neutral",
|
|
51
|
+
reasoning_payload="none",
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def request_profile_for_config(config: ProviderProfileConfig) -> ProviderRequestProfile:
|
|
56
|
+
slug = config.provider_slug
|
|
57
|
+
endpoint = _normalize_endpoint(config.base_url)
|
|
58
|
+
model = config.model.strip().lower()
|
|
59
|
+
if _uses_neutral_profile(slug):
|
|
60
|
+
return _NO_REASONING_PROFILE
|
|
61
|
+
if _uses_deepseek_profile(slug, endpoint, model):
|
|
62
|
+
return _DEEPSEEK_PROFILE
|
|
63
|
+
if _uses_openrouter_profile(slug, endpoint):
|
|
64
|
+
return _OPENROUTER_PROFILE
|
|
65
|
+
if _uses_openai_compatible_profile(slug, endpoint):
|
|
66
|
+
return _GENERIC_OPENAI_COMPATIBLE_PROFILE
|
|
67
|
+
return _GENERIC_OPENAI_COMPATIBLE_PROFILE
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _uses_neutral_profile(slug: str) -> bool:
|
|
71
|
+
return slug in {LLAMA_CPP_PROVIDER_SLUG, "pollinations"}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _uses_deepseek_profile(slug: str, endpoint: str, model: str) -> bool:
|
|
75
|
+
return (
|
|
76
|
+
slug == "deepseek"
|
|
77
|
+
or endpoint in _DEEPSEEK_ENDPOINTS
|
|
78
|
+
or (model.startswith("deepseek-") and "deepseek.com" in endpoint)
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _uses_openrouter_profile(slug: str, endpoint: str) -> bool:
|
|
83
|
+
return slug == "openrouter" or endpoint == _OPENROUTER_ENDPOINT
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _uses_openai_compatible_profile(slug: str, endpoint: str) -> bool:
|
|
87
|
+
return slug in {"openai", "openai-codex"} or endpoint == _OPENAI_ENDPOINT
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def reasoning_payload_for_profile(
|
|
91
|
+
profile: ProviderRequestProfile,
|
|
92
|
+
requested_level: str,
|
|
93
|
+
supported_levels: Iterable[str],
|
|
94
|
+
) -> dict[str, object]:
|
|
95
|
+
level = _select_reasoning_level(requested_level, supported_levels)
|
|
96
|
+
if level is None or profile.reasoning_payload == "none":
|
|
97
|
+
return {}
|
|
98
|
+
if profile.reasoning_payload == "deepseek":
|
|
99
|
+
effort = "max" if level == "xhigh" else "high"
|
|
100
|
+
return {"extra_body": {"thinking": {"type": "enabled"}}, "reasoning_effort": effort}
|
|
101
|
+
if profile.reasoning_payload == "openrouter":
|
|
102
|
+
return {"extra_body": {"reasoning": {"effort": level}}}
|
|
103
|
+
return {"reasoning_effort": level}
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def reasoning_payload_for_config(
|
|
107
|
+
config: ReasoningPayloadConfig,
|
|
108
|
+
supported_levels: Iterable[str],
|
|
109
|
+
) -> tuple[dict[str, object], bool]:
|
|
110
|
+
profile = request_profile_for_config(config)
|
|
111
|
+
payload = reasoning_payload_for_profile(profile, config.reasoning_level, supported_levels)
|
|
112
|
+
suppress_temperature = profile.suppress_temperature_when_reasoning and bool(payload)
|
|
113
|
+
return payload, suppress_temperature
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _select_reasoning_level(requested_level: str, supported_levels: Iterable[str]) -> str | None:
|
|
117
|
+
choices = tuple(level for level in supported_levels if level in _REASONING_ORDER)
|
|
118
|
+
if not choices:
|
|
119
|
+
return None
|
|
120
|
+
normalized = requested_level.casefold()
|
|
121
|
+
if normalized in choices:
|
|
122
|
+
return normalized
|
|
123
|
+
return choices[0]
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _normalize_endpoint(base_url: str) -> str:
|
|
127
|
+
return base_url.strip().rstrip("/")
|