lionagi 0.18.0__py3-none-any.whl → 0.18.2__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.
- lionagi/__init__.py +102 -59
 - lionagi/_errors.py +0 -5
 - lionagi/adapters/spec_adapters/__init__.py +9 -0
 - lionagi/adapters/spec_adapters/_protocol.py +236 -0
 - lionagi/adapters/spec_adapters/pydantic_field.py +158 -0
 - lionagi/fields.py +83 -0
 - lionagi/ln/__init__.py +3 -1
 - lionagi/ln/_async_call.py +2 -2
 - lionagi/ln/concurrency/primitives.py +4 -4
 - lionagi/ln/concurrency/task.py +1 -0
 - lionagi/ln/fuzzy/_fuzzy_match.py +2 -2
 - lionagi/ln/types/__init__.py +51 -0
 - lionagi/ln/types/_sentinel.py +154 -0
 - lionagi/ln/{types.py → types/base.py} +108 -168
 - lionagi/ln/types/operable.py +221 -0
 - lionagi/ln/types/spec.py +441 -0
 - lionagi/models/field_model.py +69 -7
 - lionagi/models/hashable_model.py +2 -3
 - lionagi/models/model_params.py +4 -3
 - lionagi/operations/ReAct/ReAct.py +1 -1
 - lionagi/operations/act/act.py +3 -3
 - lionagi/operations/builder.py +5 -7
 - lionagi/operations/fields.py +380 -0
 - lionagi/operations/flow.py +4 -6
 - lionagi/operations/node.py +4 -4
 - lionagi/operations/operate/operate.py +123 -89
 - lionagi/operations/operate/operative.py +198 -0
 - lionagi/operations/operate/step.py +203 -0
 - lionagi/operations/select/select.py +1 -1
 - lionagi/operations/select/utils.py +7 -1
 - lionagi/operations/types.py +7 -7
 - lionagi/protocols/action/manager.py +5 -6
 - lionagi/protocols/contracts.py +2 -2
 - lionagi/protocols/generic/__init__.py +22 -0
 - lionagi/protocols/generic/element.py +36 -127
 - lionagi/protocols/generic/pile.py +9 -10
 - lionagi/protocols/generic/progression.py +23 -22
 - lionagi/protocols/graph/edge.py +6 -5
 - lionagi/protocols/ids.py +6 -49
 - lionagi/protocols/messages/__init__.py +3 -1
 - lionagi/protocols/messages/base.py +7 -6
 - lionagi/protocols/messages/instruction.py +0 -1
 - lionagi/protocols/messages/message.py +2 -2
 - lionagi/protocols/types.py +1 -11
 - lionagi/service/connections/__init__.py +3 -0
 - lionagi/service/connections/providers/claude_code_cli.py +3 -2
 - lionagi/service/hooks/_types.py +1 -1
 - lionagi/service/hooks/_utils.py +1 -1
 - lionagi/service/hooks/hook_event.py +3 -8
 - lionagi/service/hooks/hook_registry.py +5 -5
 - lionagi/service/hooks/hooked_event.py +61 -1
 - lionagi/service/imodel.py +24 -20
 - lionagi/service/third_party/claude_code.py +1 -2
 - lionagi/service/third_party/openai_models.py +24 -22
 - lionagi/service/token_calculator.py +1 -94
 - lionagi/session/branch.py +26 -228
 - lionagi/session/session.py +5 -90
 - lionagi/version.py +1 -1
 - {lionagi-0.18.0.dist-info → lionagi-0.18.2.dist-info}/METADATA +6 -5
 - {lionagi-0.18.0.dist-info → lionagi-0.18.2.dist-info}/RECORD +62 -82
 - lionagi/fields/__init__.py +0 -47
 - lionagi/fields/action.py +0 -188
 - lionagi/fields/base.py +0 -153
 - lionagi/fields/code.py +0 -239
 - lionagi/fields/file.py +0 -234
 - lionagi/fields/instruct.py +0 -135
 - lionagi/fields/reason.py +0 -55
 - lionagi/fields/research.py +0 -52
 - lionagi/operations/brainstorm/__init__.py +0 -2
 - lionagi/operations/brainstorm/brainstorm.py +0 -498
 - lionagi/operations/brainstorm/prompt.py +0 -11
 - lionagi/operations/instruct/__init__.py +0 -2
 - lionagi/operations/instruct/instruct.py +0 -28
 - lionagi/operations/plan/__init__.py +0 -6
 - lionagi/operations/plan/plan.py +0 -386
 - lionagi/operations/plan/prompt.py +0 -25
 - lionagi/operations/utils.py +0 -45
 - lionagi/protocols/forms/__init__.py +0 -2
 - lionagi/protocols/forms/base.py +0 -85
 - lionagi/protocols/forms/flow.py +0 -79
 - lionagi/protocols/forms/form.py +0 -86
 - lionagi/protocols/forms/report.py +0 -48
 - lionagi/protocols/mail/__init__.py +0 -2
 - lionagi/protocols/mail/exchange.py +0 -220
 - lionagi/protocols/mail/mail.py +0 -51
 - lionagi/protocols/mail/mailbox.py +0 -103
 - lionagi/protocols/mail/manager.py +0 -218
 - lionagi/protocols/mail/package.py +0 -101
 - lionagi/protocols/operatives/__init__.py +0 -2
 - lionagi/protocols/operatives/operative.py +0 -362
 - lionagi/protocols/operatives/step.py +0 -227
 - {lionagi-0.18.0.dist-info → lionagi-0.18.2.dist-info}/WHEEL +0 -0
 - {lionagi-0.18.0.dist-info → lionagi-0.18.2.dist-info}/licenses/LICENSE +0 -0
 
| 
         @@ -5,7 +5,7 @@ import anyio 
     | 
|
| 
       5 
5 
     | 
    
         
             
            from pydantic import PrivateAttr
         
     | 
| 
       6 
6 
     | 
    
         | 
| 
       7 
7 
     | 
    
         
             
            from lionagi.ln import get_cancelled_exc_class
         
     | 
| 
       8 
     | 
    
         
            -
            from lionagi.protocols.types import DataLogger, Event, EventStatus 
     | 
| 
      
 8 
     | 
    
         
            +
            from lionagi.protocols.types import DataLogger, Event, EventStatus
         
     | 
| 
       9 
9 
     | 
    
         
             
            from lionagi.service.hooks import HookEvent, HookEventTypes
         
     | 
| 
       10 
10 
     | 
    
         | 
| 
       11 
11 
     | 
    
         
             
            global_hook_logger = DataLogger(
         
     | 
| 
         @@ -43,6 +43,16 @@ class HookedEvent(Event): 
     | 
|
| 
       43 
43 
     | 
    
         
             
                        self.execution.status = EventStatus.PROCESSING
         
     | 
| 
       44 
44 
     | 
    
         
             
                        if h_ev := self._pre_invoke_hook_event:
         
     | 
| 
       45 
45 
     | 
    
         
             
                            await h_ev.invoke()
         
     | 
| 
      
 46 
     | 
    
         
            +
             
     | 
| 
      
 47 
     | 
    
         
            +
                            # Check if hook failed or was cancelled - propagate to main event
         
     | 
| 
      
 48 
     | 
    
         
            +
                            if h_ev.execution.status in (
         
     | 
| 
      
 49 
     | 
    
         
            +
                                EventStatus.FAILED,
         
     | 
| 
      
 50 
     | 
    
         
            +
                                EventStatus.CANCELLED,
         
     | 
| 
      
 51 
     | 
    
         
            +
                            ):
         
     | 
| 
      
 52 
     | 
    
         
            +
                                self.execution.status = h_ev.execution.status
         
     | 
| 
      
 53 
     | 
    
         
            +
                                self.execution.error = f"Pre-invoke hook {h_ev.execution.status.value}: {h_ev.execution.error}"
         
     | 
| 
      
 54 
     | 
    
         
            +
                                return
         
     | 
| 
      
 55 
     | 
    
         
            +
             
     | 
| 
       46 
56 
     | 
    
         
             
                            if h_ev._should_exit:
         
     | 
| 
       47 
57 
     | 
    
         
             
                                raise h_ev._exit_cause or RuntimeError(
         
     | 
| 
       48 
58 
     | 
    
         
             
                                    "Pre-invocation hook requested exit without a cause"
         
     | 
| 
         @@ -53,6 +63,19 @@ class HookedEvent(Event): 
     | 
|
| 
       53 
63 
     | 
    
         | 
| 
       54 
64 
     | 
    
         
             
                        if h_ev := self._post_invoke_hook_event:
         
     | 
| 
       55 
65 
     | 
    
         
             
                            await h_ev.invoke()
         
     | 
| 
      
 66 
     | 
    
         
            +
             
     | 
| 
      
 67 
     | 
    
         
            +
                            # Check if hook failed or was cancelled - propagate to main event
         
     | 
| 
      
 68 
     | 
    
         
            +
                            if h_ev.execution.status in (
         
     | 
| 
      
 69 
     | 
    
         
            +
                                EventStatus.FAILED,
         
     | 
| 
      
 70 
     | 
    
         
            +
                                EventStatus.CANCELLED,
         
     | 
| 
      
 71 
     | 
    
         
            +
                            ):
         
     | 
| 
      
 72 
     | 
    
         
            +
                                self.execution.status = h_ev.execution.status
         
     | 
| 
      
 73 
     | 
    
         
            +
                                self.execution.error = f"Post-invoke hook {h_ev.execution.status.value}: {h_ev.execution.error}"
         
     | 
| 
      
 74 
     | 
    
         
            +
                                self.execution.response = (
         
     | 
| 
      
 75 
     | 
    
         
            +
                                    response  # Keep response even if hook failed
         
     | 
| 
      
 76 
     | 
    
         
            +
                                )
         
     | 
| 
      
 77 
     | 
    
         
            +
                                return
         
     | 
| 
      
 78 
     | 
    
         
            +
             
     | 
| 
       56 
79 
     | 
    
         
             
                            if h_ev._should_exit:
         
     | 
| 
       57 
80 
     | 
    
         
             
                                raise h_ev._exit_cause or RuntimeError(
         
     | 
| 
       58 
81 
     | 
    
         
             
                                    "Post-invocation hook requested exit without a cause"
         
     | 
| 
         @@ -87,10 +110,47 @@ class HookedEvent(Event): 
     | 
|
| 
       87 
110 
     | 
    
         
             
                    try:
         
     | 
| 
       88 
111 
     | 
    
         
             
                        self.execution.status = EventStatus.PROCESSING
         
     | 
| 
       89 
112 
     | 
    
         | 
| 
      
 113 
     | 
    
         
            +
                        # Execute pre-invoke hook if present
         
     | 
| 
      
 114 
     | 
    
         
            +
                        if h_ev := self._pre_invoke_hook_event:
         
     | 
| 
      
 115 
     | 
    
         
            +
                            await h_ev.invoke()
         
     | 
| 
      
 116 
     | 
    
         
            +
             
     | 
| 
      
 117 
     | 
    
         
            +
                            # Check if hook failed or was cancelled - propagate to main event
         
     | 
| 
      
 118 
     | 
    
         
            +
                            if h_ev.execution.status in (
         
     | 
| 
      
 119 
     | 
    
         
            +
                                EventStatus.FAILED,
         
     | 
| 
      
 120 
     | 
    
         
            +
                                EventStatus.CANCELLED,
         
     | 
| 
      
 121 
     | 
    
         
            +
                            ):
         
     | 
| 
      
 122 
     | 
    
         
            +
                                self.execution.status = h_ev.execution.status
         
     | 
| 
      
 123 
     | 
    
         
            +
                                self.execution.error = f"Pre-invoke hook {h_ev.execution.status.value}: {h_ev.execution.error}"
         
     | 
| 
      
 124 
     | 
    
         
            +
                                return
         
     | 
| 
      
 125 
     | 
    
         
            +
             
     | 
| 
      
 126 
     | 
    
         
            +
                            if h_ev._should_exit:
         
     | 
| 
      
 127 
     | 
    
         
            +
                                raise h_ev._exit_cause or RuntimeError(
         
     | 
| 
      
 128 
     | 
    
         
            +
                                    "Pre-invocation hook requested exit without a cause"
         
     | 
| 
      
 129 
     | 
    
         
            +
                                )
         
     | 
| 
      
 130 
     | 
    
         
            +
                            await global_hook_logger.alog(h_ev)
         
     | 
| 
      
 131 
     | 
    
         
            +
             
     | 
| 
       90 
132 
     | 
    
         
             
                        async for chunk in self._stream():
         
     | 
| 
       91 
133 
     | 
    
         
             
                            response.append(chunk)
         
     | 
| 
       92 
134 
     | 
    
         
             
                            yield chunk
         
     | 
| 
       93 
135 
     | 
    
         | 
| 
      
 136 
     | 
    
         
            +
                        # Execute post-invoke hook if present
         
     | 
| 
      
 137 
     | 
    
         
            +
                        if h_ev := self._post_invoke_hook_event:
         
     | 
| 
      
 138 
     | 
    
         
            +
                            await h_ev.invoke()
         
     | 
| 
      
 139 
     | 
    
         
            +
             
     | 
| 
      
 140 
     | 
    
         
            +
                            # Check if hook failed or was cancelled - don't fail the stream since data was already sent
         
     | 
| 
      
 141 
     | 
    
         
            +
                            if h_ev.execution.status in (
         
     | 
| 
      
 142 
     | 
    
         
            +
                                EventStatus.FAILED,
         
     | 
| 
      
 143 
     | 
    
         
            +
                                EventStatus.CANCELLED,
         
     | 
| 
      
 144 
     | 
    
         
            +
                            ):
         
     | 
| 
      
 145 
     | 
    
         
            +
                                # Log but don't fail the stream
         
     | 
| 
      
 146 
     | 
    
         
            +
                                await global_hook_logger.alog(h_ev)
         
     | 
| 
      
 147 
     | 
    
         
            +
                            elif h_ev._should_exit:
         
     | 
| 
      
 148 
     | 
    
         
            +
                                raise h_ev._exit_cause or RuntimeError(
         
     | 
| 
      
 149 
     | 
    
         
            +
                                    "Post-invocation hook requested exit without a cause"
         
     | 
| 
      
 150 
     | 
    
         
            +
                                )
         
     | 
| 
      
 151 
     | 
    
         
            +
                            else:
         
     | 
| 
      
 152 
     | 
    
         
            +
                                await global_hook_logger.alog(h_ev)
         
     | 
| 
      
 153 
     | 
    
         
            +
             
     | 
| 
       94 
154 
     | 
    
         
             
                        self.execution.response = response
         
     | 
| 
       95 
155 
     | 
    
         
             
                        self.execution.status = EventStatus.COMPLETED
         
     | 
| 
       96 
156 
     | 
    
         | 
    
        lionagi/service/imodel.py
    CHANGED
    
    | 
         @@ -3,19 +3,22 @@ 
     | 
|
| 
       3 
3 
     | 
    
         | 
| 
       4 
4 
     | 
    
         
             
            import asyncio
         
     | 
| 
       5 
5 
     | 
    
         
             
            from collections.abc import AsyncGenerator, Callable
         
     | 
| 
      
 6 
     | 
    
         
            +
            from typing import Any
         
     | 
| 
      
 7 
     | 
    
         
            +
            from uuid import UUID, uuid4
         
     | 
| 
       6 
8 
     | 
    
         | 
| 
       7 
9 
     | 
    
         
             
            from pydantic import BaseModel
         
     | 
| 
       8 
10 
     | 
    
         | 
| 
       9 
11 
     | 
    
         
             
            from lionagi.ln import is_coro_func, now_utc
         
     | 
| 
       10 
     | 
    
         
            -
            from lionagi.protocols.generic 
     | 
| 
       11 
     | 
    
         
            -
             
     | 
| 
       12 
     | 
    
         
            -
            from  
     | 
| 
       13 
     | 
    
         
            -
            from  
     | 
| 
       14 
     | 
    
         
            -
             
     | 
| 
       15 
     | 
    
         
            -
             
     | 
| 
       16 
     | 
    
         
            -
             
     | 
| 
       17 
     | 
    
         
            -
             
     | 
| 
       18 
     | 
    
         
            -
             
     | 
| 
      
 12 
     | 
    
         
            +
            from lionagi.protocols.generic import ID, Event, EventStatus, Log
         
     | 
| 
      
 13 
     | 
    
         
            +
             
     | 
| 
      
 14 
     | 
    
         
            +
            from .connections import APICalling, Endpoint, match_endpoint
         
     | 
| 
      
 15 
     | 
    
         
            +
            from .hooks import (
         
     | 
| 
      
 16 
     | 
    
         
            +
                HookedEvent,
         
     | 
| 
      
 17 
     | 
    
         
            +
                HookEvent,
         
     | 
| 
      
 18 
     | 
    
         
            +
                HookEventTypes,
         
     | 
| 
      
 19 
     | 
    
         
            +
                HookRegistry,
         
     | 
| 
      
 20 
     | 
    
         
            +
                global_hook_logger,
         
     | 
| 
      
 21 
     | 
    
         
            +
            )
         
     | 
| 
       19 
22 
     | 
    
         
             
            from .rate_limited_processor import RateLimitedAPIExecutor
         
     | 
| 
       20 
23 
     | 
    
         | 
| 
       21 
24 
     | 
    
         | 
| 
         @@ -52,7 +55,7 @@ class iModel: 
     | 
|
| 
       52 
55 
     | 
    
         
             
                    provider_metadata: dict | None = None,
         
     | 
| 
       53 
56 
     | 
    
         
             
                    hook_registry: HookRegistry | dict | None = None,
         
     | 
| 
       54 
57 
     | 
    
         
             
                    exit_hook: bool = False,
         
     | 
| 
       55 
     | 
    
         
            -
                    id:  
     | 
| 
      
 58 
     | 
    
         
            +
                    id: UUID | str = None,
         
     | 
| 
       56 
59 
     | 
    
         
             
                    created_at: float | None = None,
         
     | 
| 
       57 
60 
     | 
    
         
             
                    **kwargs,
         
     | 
| 
       58 
61 
     | 
    
         
             
                ) -> None:
         
     | 
| 
         @@ -100,7 +103,7 @@ class iModel: 
     | 
|
| 
       100 
103 
     | 
    
         
             
                    if id is not None:
         
     | 
| 
       101 
104 
     | 
    
         
             
                        self.id = ID.get_id(id)
         
     | 
| 
       102 
105 
     | 
    
         
             
                    else:
         
     | 
| 
       103 
     | 
    
         
            -
                        self.id =  
     | 
| 
      
 106 
     | 
    
         
            +
                        self.id = uuid4()
         
     | 
| 
       104 
107 
     | 
    
         
             
                    if created_at is not None:
         
     | 
| 
       105 
108 
     | 
    
         
             
                        if not isinstance(created_at, float):
         
     | 
| 
       106 
109 
     | 
    
         
             
                            raise ValueError("created_at must be a float timestamp.")
         
     | 
| 
         @@ -270,7 +273,7 @@ class iModel: 
     | 
|
| 
       270 
273 
     | 
    
         
             
                        include_token_usage_to_model=include_token_usage_to_model,
         
     | 
| 
       271 
274 
     | 
    
         
             
                    )
         
     | 
| 
       272 
275 
     | 
    
         | 
| 
       273 
     | 
    
         
            -
                async def process_chunk(self, chunk) ->  
     | 
| 
      
 276 
     | 
    
         
            +
                async def process_chunk(self, chunk) -> Any:
         
     | 
| 
       274 
277 
     | 
    
         
             
                    """Processes a chunk of streaming data.
         
     | 
| 
       275 
278 
     | 
    
         | 
| 
       276 
279 
     | 
    
         
             
                    Override this method in subclasses if you need custom handling
         
     | 
| 
         @@ -284,6 +287,7 @@ class iModel: 
     | 
|
| 
       284 
287 
     | 
    
         
             
                        if is_coro_func(self.streaming_process_func):
         
     | 
| 
       285 
288 
     | 
    
         
             
                            return await self.streaming_process_func(chunk)
         
     | 
| 
       286 
289 
     | 
    
         
             
                        return self.streaming_process_func(chunk)
         
     | 
| 
      
 290 
     | 
    
         
            +
                    return None
         
     | 
| 
       287 
291 
     | 
    
         | 
| 
       288 
292 
     | 
    
         
             
                async def stream(self, api_call=None, **kw) -> AsyncGenerator:
         
     | 
| 
       289 
293 
     | 
    
         
             
                    """Performs a streaming API call with the given arguments.
         
     | 
| 
         @@ -313,8 +317,8 @@ class iModel: 
     | 
|
| 
       313 
317 
     | 
    
         
             
                            try:
         
     | 
| 
       314 
318 
     | 
    
         
             
                                async for i in api_call.stream():
         
     | 
| 
       315 
319 
     | 
    
         
             
                                    result = await self.process_chunk(i)
         
     | 
| 
       316 
     | 
    
         
            -
                                    if  
     | 
| 
       317 
     | 
    
         
            -
             
     | 
| 
      
 320 
     | 
    
         
            +
                                    # Yield processed result if available, otherwise yield raw chunk
         
     | 
| 
      
 321 
     | 
    
         
            +
                                    yield result if result is not None else i
         
     | 
| 
       318 
322 
     | 
    
         
             
                            except Exception as e:
         
     | 
| 
       319 
323 
     | 
    
         
             
                                raise ValueError(f"Failed to stream API call: {e}")
         
     | 
| 
       320 
324 
     | 
    
         
             
                            finally:
         
     | 
| 
         @@ -323,8 +327,8 @@ class iModel: 
     | 
|
| 
       323 
327 
     | 
    
         
             
                        try:
         
     | 
| 
       324 
328 
     | 
    
         
             
                            async for i in api_call.stream():
         
     | 
| 
       325 
329 
     | 
    
         
             
                                result = await self.process_chunk(i)
         
     | 
| 
       326 
     | 
    
         
            -
                                if  
     | 
| 
       327 
     | 
    
         
            -
             
     | 
| 
      
 330 
     | 
    
         
            +
                                # Yield processed result if available, otherwise yield raw chunk
         
     | 
| 
      
 331 
     | 
    
         
            +
                                yield result if result is not None else i
         
     | 
| 
       328 
332 
     | 
    
         
             
                        except Exception as e:
         
     | 
| 
       329 
333 
     | 
    
         
             
                            raise ValueError(f"Failed to stream API call: {e}")
         
     | 
| 
       330 
334 
     | 
    
         
             
                        finally:
         
     | 
| 
         @@ -360,10 +364,10 @@ class iModel: 
     | 
|
| 
       360 
364 
     | 
    
         
             
                        await self.executor.append(api_call)
         
     | 
| 
       361 
365 
     | 
    
         
             
                        await self.executor.forward()
         
     | 
| 
       362 
366 
     | 
    
         
             
                        ctr = 0
         
     | 
| 
       363 
     | 
    
         
            -
                        while api_call.status  
     | 
| 
       364 
     | 
    
         
            -
                            EventStatus. 
     | 
| 
       365 
     | 
    
         
            -
                            EventStatus. 
     | 
| 
       366 
     | 
    
         
            -
                         
     | 
| 
      
 367 
     | 
    
         
            +
                        while api_call.status in [
         
     | 
| 
      
 368 
     | 
    
         
            +
                            EventStatus.PROCESSING,
         
     | 
| 
      
 369 
     | 
    
         
            +
                            EventStatus.PENDING,
         
     | 
| 
      
 370 
     | 
    
         
            +
                        ]:
         
     | 
| 
       367 
371 
     | 
    
         
             
                            if ctr > 100:
         
     | 
| 
       368 
372 
     | 
    
         
             
                                break
         
     | 
| 
       369 
373 
     | 
    
         
             
                            await self.executor.forward()
         
     | 
| 
         @@ -22,7 +22,6 @@ from pydantic import BaseModel, Field, field_validator, model_validator 
     | 
|
| 
       22 
22 
     | 
    
         | 
| 
       23 
23 
     | 
    
         
             
            from lionagi import ln
         
     | 
| 
       24 
24 
     | 
    
         
             
            from lionagi.libs.schema.as_readable import as_readable
         
     | 
| 
       25 
     | 
    
         
            -
            from lionagi.utils import is_coro_func
         
     | 
| 
       26 
25 
     | 
    
         | 
| 
       27 
26 
     | 
    
         
             
            HAS_CLAUDE_CODE_CLI = False
         
     | 
| 
       28 
27 
     | 
    
         
             
            CLAUDE_CLI = None
         
     | 
| 
         @@ -556,7 +555,7 @@ def _pp_final(sess: ClaudeSession, theme) -> None: 
     | 
|
| 
       556 
555 
     | 
    
         
             
            async def _maybe_await(func, *args, **kw):
         
     | 
| 
       557 
556 
     | 
    
         
             
                """Call func which may be sync or async."""
         
     | 
| 
       558 
557 
     | 
    
         
             
                res = func(*args, **kw) if func else None
         
     | 
| 
       559 
     | 
    
         
            -
                if is_coro_func(res):
         
     | 
| 
      
 558 
     | 
    
         
            +
                if ln.is_coro_func(res):
         
     | 
| 
       560 
559 
     | 
    
         
             
                    await res
         
     | 
| 
       561 
560 
     | 
    
         | 
| 
       562 
561 
     | 
    
         | 
| 
         @@ -145,7 +145,7 @@ class ImageURLObject(BaseModel): 
     | 
|
| 
       145 
145 
     | 
    
         
             
                """Image URL object; 'detail' is optional and model-dependent."""
         
     | 
| 
       146 
146 
     | 
    
         | 
| 
       147 
147 
     | 
    
         
             
                url: str
         
     | 
| 
       148 
     | 
    
         
            -
                detail:  
     | 
| 
      
 148 
     | 
    
         
            +
                detail: Literal["auto", "low", "high"] | None = Field(
         
     | 
| 
       149 
149 
     | 
    
         
             
                    default=None,
         
     | 
| 
       150 
150 
     | 
    
         
             
                    description="Optional detail control for vision models (auto/low/high).",
         
     | 
| 
       151 
151 
     | 
    
         
             
                )
         
     | 
| 
         @@ -168,8 +168,8 @@ class FunctionDef(BaseModel): 
     | 
|
| 
       168 
168 
     | 
    
         
             
                """JSON Schema function definition for tool-calling."""
         
     | 
| 
       169 
169 
     | 
    
         | 
| 
       170 
170 
     | 
    
         
             
                name: str
         
     | 
| 
       171 
     | 
    
         
            -
                description:  
     | 
| 
       172 
     | 
    
         
            -
                parameters:  
     | 
| 
      
 171 
     | 
    
         
            +
                description: str | None = None
         
     | 
| 
      
 172 
     | 
    
         
            +
                parameters: dict[str, Any] = Field(
         
     | 
| 
       173 
173 
     | 
    
         
             
                    default_factory=dict,
         
     | 
| 
       174 
174 
     | 
    
         
             
                    description="JSON Schema describing function parameters.",
         
     | 
| 
       175 
175 
     | 
    
         
             
                )
         
     | 
| 
         @@ -204,7 +204,7 @@ class ToolChoiceFunction(BaseModel): 
     | 
|
| 
       204 
204 
     | 
    
         
             
                """Explicit tool selection."""
         
     | 
| 
       205 
205 
     | 
    
         | 
| 
       206 
206 
     | 
    
         
             
                type: Literal["function"] = "function"
         
     | 
| 
       207 
     | 
    
         
            -
                function:  
     | 
| 
      
 207 
     | 
    
         
            +
                function: dict[str, str]  # {"name": "<function_name>"}
         
     | 
| 
       208 
208 
     | 
    
         | 
| 
       209 
209 
     | 
    
         | 
| 
       210 
210 
     | 
    
         
             
            ToolChoice = Union[Literal["auto", "none"], ToolChoiceFunction]
         
     | 
| 
         @@ -223,12 +223,16 @@ class ResponseFormatJSONObject(BaseModel): 
     | 
|
| 
       223 
223 
     | 
    
         | 
| 
       224 
224 
     | 
    
         
             
            class JSONSchemaFormat(BaseModel):
         
     | 
| 
       225 
225 
     | 
    
         
             
                name: str
         
     | 
| 
       226 
     | 
    
         
            -
                 
     | 
| 
       227 
     | 
    
         
            -
             
     | 
| 
      
 226 
     | 
    
         
            +
                schema_: dict[str, Any] = Field(
         
     | 
| 
      
 227 
     | 
    
         
            +
                    alias="schema", description="JSON Schema definition"
         
     | 
| 
      
 228 
     | 
    
         
            +
                )
         
     | 
| 
      
 229 
     | 
    
         
            +
                strict: bool | None = Field(
         
     | 
| 
       228 
230 
     | 
    
         
             
                    default=None,
         
     | 
| 
       229 
231 
     | 
    
         
             
                    description="If true, disallow unspecified properties (strict schema).",
         
     | 
| 
       230 
232 
     | 
    
         
             
                )
         
     | 
| 
       231 
233 
     | 
    
         | 
| 
      
 234 
     | 
    
         
            +
                model_config = {"populate_by_name": True}
         
     | 
| 
      
 235 
     | 
    
         
            +
             
     | 
| 
       232 
236 
     | 
    
         | 
| 
       233 
237 
     | 
    
         
             
            class ResponseFormatJSONSchema(BaseModel):
         
     | 
| 
       234 
238 
     | 
    
         
             
                type: Literal["json_schema"] = "json_schema"
         
     | 
| 
         @@ -247,31 +251,29 @@ ResponseFormat = Union[ 
     | 
|
| 
       247 
251 
     | 
    
         | 
| 
       248 
252 
     | 
    
         
             
            class SystemMessage(BaseModel):
         
     | 
| 
       249 
253 
     | 
    
         
             
                role: Literal[ChatRole.system] = ChatRole.system
         
     | 
| 
       250 
     | 
    
         
            -
                content:  
     | 
| 
       251 
     | 
    
         
            -
                name:  
     | 
| 
      
 254 
     | 
    
         
            +
                content: str | list[ContentPart]
         
     | 
| 
      
 255 
     | 
    
         
            +
                name: str | None = None  # optional per API
         
     | 
| 
       252 
256 
     | 
    
         | 
| 
       253 
257 
     | 
    
         | 
| 
       254 
258 
     | 
    
         
             
            class DeveloperMessage(BaseModel):
         
     | 
| 
       255 
259 
     | 
    
         
             
                role: Literal[ChatRole.developer] = ChatRole.developer
         
     | 
| 
       256 
     | 
    
         
            -
                content:  
     | 
| 
       257 
     | 
    
         
            -
                name:  
     | 
| 
      
 260 
     | 
    
         
            +
                content: str | list[ContentPart]
         
     | 
| 
      
 261 
     | 
    
         
            +
                name: str | None = None
         
     | 
| 
       258 
262 
     | 
    
         | 
| 
       259 
263 
     | 
    
         | 
| 
       260 
264 
     | 
    
         
             
            class UserMessage(BaseModel):
         
     | 
| 
       261 
265 
     | 
    
         
             
                role: Literal[ChatRole.user] = ChatRole.user
         
     | 
| 
       262 
     | 
    
         
            -
                content:  
     | 
| 
       263 
     | 
    
         
            -
                name:  
     | 
| 
      
 266 
     | 
    
         
            +
                content: str | list[ContentPart]
         
     | 
| 
      
 267 
     | 
    
         
            +
                name: str | None = None
         
     | 
| 
       264 
268 
     | 
    
         | 
| 
       265 
269 
     | 
    
         | 
| 
       266 
270 
     | 
    
         
             
            class AssistantMessage(BaseModel):
         
     | 
| 
       267 
271 
     | 
    
         
             
                role: Literal[ChatRole.assistant] = ChatRole.assistant
         
     | 
| 
       268 
272 
     | 
    
         
             
                # Either textual content, or only tool_calls (when asking you to call tools)
         
     | 
| 
       269 
     | 
    
         
            -
                content:  
     | 
| 
       270 
     | 
    
         
            -
                name:  
     | 
| 
       271 
     | 
    
         
            -
                tool_calls:  
     | 
| 
       272 
     | 
    
         
            -
                function_call:  
     | 
| 
       273 
     | 
    
         
            -
                    None  # legacy function-calling result
         
     | 
| 
       274 
     | 
    
         
            -
                )
         
     | 
| 
      
 273 
     | 
    
         
            +
                content: str | list[ContentPart] | None = None
         
     | 
| 
      
 274 
     | 
    
         
            +
                name: str | None = None
         
     | 
| 
      
 275 
     | 
    
         
            +
                tool_calls: list[ToolCall] | None = None  # modern tool-calling result
         
     | 
| 
      
 276 
     | 
    
         
            +
                function_call: FunctionCall | None = None  # legacy function-calling result
         
     | 
| 
       275 
277 
     | 
    
         | 
| 
       276 
278 
     | 
    
         | 
| 
       277 
279 
     | 
    
         
             
            class ToolMessage(BaseModel):
         
     | 
| 
         @@ -292,7 +294,7 @@ ChatMessage = ( 
     | 
|
| 
       292 
294 
     | 
    
         | 
| 
       293 
295 
     | 
    
         | 
| 
       294 
296 
     | 
    
         
             
            class StreamOptions(BaseModel):
         
     | 
| 
       295 
     | 
    
         
            -
                include_usage:  
     | 
| 
      
 297 
     | 
    
         
            +
                include_usage: bool | None = Field(
         
     | 
| 
       296 
298 
     | 
    
         
             
                    default=None,
         
     | 
| 
       297 
299 
     | 
    
         
             
                    description="If true, a final streamed chunk includes token usage.",
         
     | 
| 
       298 
300 
     | 
    
         
             
                )
         
     | 
| 
         @@ -309,7 +311,7 @@ class OpenAIChatCompletionsRequest(BaseModel): 
     | 
|
| 
       309 
311 
     | 
    
         | 
| 
       310 
312 
     | 
    
         
             
                # Required
         
     | 
| 
       311 
313 
     | 
    
         
             
                model: str = Field(..., description="Model name, e.g., 'gpt-4o', 'gpt-4o-mini'.")  # type: ignore
         
     | 
| 
       312 
     | 
    
         
            -
                messages:  
     | 
| 
      
 314 
     | 
    
         
            +
                messages: list[ChatMessage] = Field(
         
     | 
| 
       313 
315 
     | 
    
         
             
                    ...,
         
     | 
| 
       314 
316 
     | 
    
         
             
                    description="Conversation so far, including system/developer context.",
         
     | 
| 
       315 
317 
     | 
    
         
             
                )
         
     | 
| 
         @@ -348,7 +350,7 @@ class OpenAIChatCompletionsRequest(BaseModel): 
     | 
|
| 
       348 
350 
     | 
    
         
             
                n: int | None = Field(
         
     | 
| 
       349 
351 
     | 
    
         
             
                    default=None, ge=1, description="# of choices to generate."
         
     | 
| 
       350 
352 
     | 
    
         
             
                )
         
     | 
| 
       351 
     | 
    
         
            -
                stop: str |  
     | 
| 
      
 353 
     | 
    
         
            +
                stop: str | list[str] | None = Field(
         
     | 
| 
       352 
354 
     | 
    
         
             
                    default=None, description="Stop sequence(s)."
         
     | 
| 
       353 
355 
     | 
    
         
             
                )
         
     | 
| 
       354 
356 
     | 
    
         
             
                logit_bias: dict[str, float] | None = Field(
         
     | 
| 
         @@ -406,7 +408,7 @@ class OpenAIChatCompletionsRequest(BaseModel): 
     | 
|
| 
       406 
408 
     | 
    
         
             
                    description="Whether to store the response server-side (model-dependent).",
         
     | 
| 
       407 
409 
     | 
    
         
             
                )
         
     | 
| 
       408 
410 
     | 
    
         
             
                metadata: dict[str, Any] | None = None
         
     | 
| 
       409 
     | 
    
         
            -
                reasoning_effort:  
     | 
| 
      
 411 
     | 
    
         
            +
                reasoning_effort: Literal["low", "medium", "high"] | None = Field(
         
     | 
| 
       410 
412 
     | 
    
         
             
                    default=None,
         
     | 
| 
       411 
413 
     | 
    
         
             
                    description="For reasoning models: trade-off between speed and accuracy.",
         
     | 
| 
       412 
414 
     | 
    
         
             
                )
         
     | 
| 
         @@ -1,83 +1,10 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            # Copyright (c) 2023-2025, HaiyangLi <quantocean.li at gmail dot com>
         
     | 
| 
       2 
2 
     | 
    
         
             
            # SPDX-License-Identifier: Apache-2.0
         
     | 
| 
       3 
3 
     | 
    
         | 
| 
       4 
     | 
    
         
            -
            import base64
         
     | 
| 
       5 
4 
     | 
    
         
             
            from collections.abc import Callable
         
     | 
| 
       6 
     | 
    
         
            -
            from io import BytesIO
         
     | 
| 
       7 
5 
     | 
    
         | 
| 
       8 
6 
     | 
    
         
             
            import tiktoken
         
     | 
| 
       9 
7 
     | 
    
         | 
| 
       10 
     | 
    
         
            -
            GPT4O_IMAGE_PRICING = {
         
     | 
| 
       11 
     | 
    
         
            -
                "base_cost": 85,
         
     | 
| 
       12 
     | 
    
         
            -
                "low_detail": 0,
         
     | 
| 
       13 
     | 
    
         
            -
                "max_dimension": 2048,
         
     | 
| 
       14 
     | 
    
         
            -
                "min_side": 768,
         
     | 
| 
       15 
     | 
    
         
            -
                "tile_size": 512,
         
     | 
| 
       16 
     | 
    
         
            -
                "tile_cost": 170,
         
     | 
| 
       17 
     | 
    
         
            -
            }
         
     | 
| 
       18 
     | 
    
         
            -
             
     | 
| 
       19 
     | 
    
         
            -
            GPT4O_MINI_IMAGE_PRICING = {
         
     | 
| 
       20 
     | 
    
         
            -
                "base_cost": 2833,
         
     | 
| 
       21 
     | 
    
         
            -
                "low_detail": 0,
         
     | 
| 
       22 
     | 
    
         
            -
                "max_dimension": 2048,
         
     | 
| 
       23 
     | 
    
         
            -
                "min_side": 768,
         
     | 
| 
       24 
     | 
    
         
            -
                "tile_size": 512,
         
     | 
| 
       25 
     | 
    
         
            -
                "tile_cost": 5667,
         
     | 
| 
       26 
     | 
    
         
            -
            }
         
     | 
| 
       27 
     | 
    
         
            -
             
     | 
| 
       28 
     | 
    
         
            -
            O1_IMAGE_PRICING = {
         
     | 
| 
       29 
     | 
    
         
            -
                "base_cost": 75,
         
     | 
| 
       30 
     | 
    
         
            -
                "low_detail": 0,
         
     | 
| 
       31 
     | 
    
         
            -
                "max_dimension": 2048,
         
     | 
| 
       32 
     | 
    
         
            -
                "min_side": 768,
         
     | 
| 
       33 
     | 
    
         
            -
                "tile_size": 512,
         
     | 
| 
       34 
     | 
    
         
            -
                "tile_cost": 150,
         
     | 
| 
       35 
     | 
    
         
            -
            }
         
     | 
| 
       36 
     | 
    
         
            -
             
     | 
| 
       37 
     | 
    
         
            -
             
     | 
| 
       38 
     | 
    
         
            -
            def calculate_image_token_usage_from_base64(
         
     | 
| 
       39 
     | 
    
         
            -
                image_base64: str, detail, image_pricing
         
     | 
| 
       40 
     | 
    
         
            -
            ):
         
     | 
| 
       41 
     | 
    
         
            -
                from PIL import Image
         
     | 
| 
       42 
     | 
    
         
            -
             
     | 
| 
       43 
     | 
    
         
            -
                # Decode the base64 string to get image data
         
     | 
| 
       44 
     | 
    
         
            -
                if "data:image/jpeg;base64," in image_base64:
         
     | 
| 
       45 
     | 
    
         
            -
                    image_base64 = image_base64.split("data:image/jpeg;base64,")[1]
         
     | 
| 
       46 
     | 
    
         
            -
                    image_base64.strip("{}")
         
     | 
| 
       47 
     | 
    
         
            -
             
     | 
| 
       48 
     | 
    
         
            -
                image_data = base64.b64decode(image_base64)
         
     | 
| 
       49 
     | 
    
         
            -
                image = Image.open(BytesIO(image_data))
         
     | 
| 
       50 
     | 
    
         
            -
             
     | 
| 
       51 
     | 
    
         
            -
                # Get image dimensions
         
     | 
| 
       52 
     | 
    
         
            -
                width, height = image.size
         
     | 
| 
       53 
     | 
    
         
            -
             
     | 
| 
       54 
     | 
    
         
            -
                if detail == "low":
         
     | 
| 
       55 
     | 
    
         
            -
                    return image_pricing["base_cost"] + image_pricing["low_detail"]
         
     | 
| 
       56 
     | 
    
         
            -
             
     | 
| 
       57 
     | 
    
         
            -
                # Scale to fit within a 2048 x 2048 tile
         
     | 
| 
       58 
     | 
    
         
            -
                max_dimension = image_pricing["max_dimension"]
         
     | 
| 
       59 
     | 
    
         
            -
                if width > max_dimension or height > max_dimension:
         
     | 
| 
       60 
     | 
    
         
            -
                    scale_factor = max_dimension / max(width, height)
         
     | 
| 
       61 
     | 
    
         
            -
                    width = int(width * scale_factor)
         
     | 
| 
       62 
     | 
    
         
            -
                    height = int(height * scale_factor)
         
     | 
| 
       63 
     | 
    
         
            -
             
     | 
| 
       64 
     | 
    
         
            -
                # Scale such that the shortest side is 768px
         
     | 
| 
       65 
     | 
    
         
            -
                min_side = image_pricing["min_side"]
         
     | 
| 
       66 
     | 
    
         
            -
                if min(width, height) > min_side:
         
     | 
| 
       67 
     | 
    
         
            -
                    scale_factor = min_side / min(width, height)
         
     | 
| 
       68 
     | 
    
         
            -
                    width = int(width * scale_factor)
         
     | 
| 
       69 
     | 
    
         
            -
                    height = int(height * scale_factor)
         
     | 
| 
       70 
     | 
    
         
            -
             
     | 
| 
       71 
     | 
    
         
            -
                # Calculate the number of 512px tiles
         
     | 
| 
       72 
     | 
    
         
            -
                num_tiles = (width // image_pricing["tile_size"]) * (
         
     | 
| 
       73 
     | 
    
         
            -
                    height // image_pricing["tile_size"]
         
     | 
| 
       74 
     | 
    
         
            -
                )
         
     | 
| 
       75 
     | 
    
         
            -
                token_cost = (
         
     | 
| 
       76 
     | 
    
         
            -
                    image_pricing["base_cost"] + image_pricing["tile_cost"] * num_tiles
         
     | 
| 
       77 
     | 
    
         
            -
                )
         
     | 
| 
       78 
     | 
    
         
            -
             
     | 
| 
       79 
     | 
    
         
            -
                return token_cost
         
     | 
| 
       80 
     | 
    
         
            -
             
     | 
| 
       81 
8 
     | 
    
         | 
| 
       82 
9 
     | 
    
         
             
            def get_encoding_name(value: str) -> str:
         
     | 
| 
       83 
10 
     | 
    
         
             
                try:
         
     | 
| 
         @@ -91,17 +18,6 @@ def get_encoding_name(value: str) -> str: 
     | 
|
| 
       91 
18 
     | 
    
         
             
                        return "o200k_base"
         
     | 
| 
       92 
19 
     | 
    
         | 
| 
       93 
20 
     | 
    
         | 
| 
       94 
     | 
    
         
            -
            def get_image_pricing(model: str) -> dict:
         
     | 
| 
       95 
     | 
    
         
            -
                if "gpt-4o-mini" in model:
         
     | 
| 
       96 
     | 
    
         
            -
                    return GPT4O_MINI_IMAGE_PRICING
         
     | 
| 
       97 
     | 
    
         
            -
                elif "gpt-4o" in model:
         
     | 
| 
       98 
     | 
    
         
            -
                    return GPT4O_IMAGE_PRICING
         
     | 
| 
       99 
     | 
    
         
            -
                elif "o1" in model and "mini" not in model:
         
     | 
| 
       100 
     | 
    
         
            -
                    return O1_IMAGE_PRICING
         
     | 
| 
       101 
     | 
    
         
            -
                else:
         
     | 
| 
       102 
     | 
    
         
            -
                    raise ValueError("Invalid model name")
         
     | 
| 
       103 
     | 
    
         
            -
             
     | 
| 
       104 
     | 
    
         
            -
             
     | 
| 
       105 
21 
     | 
    
         
             
            class TokenCalculator:
         
     | 
| 
       106 
22 
     | 
    
         
             
                @staticmethod
         
     | 
| 
       107 
23 
     | 
    
         
             
                def calculate_message_tokens(messages: list[dict], /, **kwargs) -> int:
         
     | 
| 
         @@ -177,16 +93,7 @@ class TokenCalculator: 
     | 
|
| 
       177 
93 
     | 
    
         
             
                            if "text" in i_:
         
     | 
| 
       178 
94 
     | 
    
         
             
                                return TokenCalculator._calculate_chatitem(str(i_["text"]))
         
     | 
| 
       179 
95 
     | 
    
         
             
                            elif "image_url" in i_:
         
     | 
| 
       180 
     | 
    
         
            -
                                 
     | 
| 
       181 
     | 
    
         
            -
                                if "data:image/jpeg;base64," in a:
         
     | 
| 
       182 
     | 
    
         
            -
                                    a = a.split("data:image/jpeg;base64,")[1].strip()
         
     | 
| 
       183 
     | 
    
         
            -
                                    pricing = get_image_pricing(model_name)
         
     | 
| 
       184 
     | 
    
         
            -
                                    return (
         
     | 
| 
       185 
     | 
    
         
            -
                                        calculate_image_token_usage_from_base64(
         
     | 
| 
       186 
     | 
    
         
            -
                                            a, i_.get("detail", "low"), pricing
         
     | 
| 
       187 
     | 
    
         
            -
                                        )
         
     | 
| 
       188 
     | 
    
         
            -
                                        + 15  # buffer for image
         
     | 
| 
       189 
     | 
    
         
            -
                                    )
         
     | 
| 
      
 96 
     | 
    
         
            +
                                return 500  # fixed cost for image URL
         
     | 
| 
       190 
97 
     | 
    
         | 
| 
       191 
98 
     | 
    
         
             
                        if isinstance(i_, list):
         
     | 
| 
       192 
99 
     | 
    
         
             
                            return sum(
         
     |