lm-deluge 0.0.70__py3-none-any.whl → 0.0.72__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.
@@ -90,9 +90,32 @@ class APIRequestBase(ABC):
90
90
  start -> poll -> result style of request.
91
91
  """
92
92
  assert self.context.status_tracker, "no status tracker"
93
- start_time = time.time()
93
+ poll_interval = 5.0
94
+ attempt_start = time.monotonic()
95
+ deadline = attempt_start + self.context.request_timeout
96
+ response_id: str | None = None
97
+ last_status: str | None = None
98
+
94
99
  async with aiohttp.ClientSession() as session:
95
- last_status: str | None = None
100
+
101
+ async def cancel_response(reason: str) -> None:
102
+ nonlocal response_id
103
+ if not response_id:
104
+ return
105
+ cancel_url = f"{self.url}/{response_id}/cancel"
106
+ try:
107
+ async with session.post(
108
+ url=cancel_url,
109
+ headers=self.request_header,
110
+ ) as cancel_response:
111
+ cancel_response.raise_for_status()
112
+ print(f"Background req {response_id} cancelled: {reason}")
113
+ except (
114
+ Exception
115
+ ) as cancel_err: # pragma: no cover - best effort logging
116
+ print(
117
+ f"Failed to cancel background req {response_id}: {cancel_err}"
118
+ )
96
119
 
97
120
  try:
98
121
  self.context.status_tracker.total_requests += 1
@@ -109,14 +132,11 @@ class APIRequestBase(ABC):
109
132
  last_status = data["status"]
110
133
 
111
134
  while True:
112
- if time.time() - start_time > self.context.request_timeout:
113
- # cancel the response
114
- async with session.post(
115
- url=f"{self.url}/{response_id}/cancel",
116
- headers=self.request_header,
117
- ) as http_response:
118
- http_response.raise_for_status()
119
-
135
+ now = time.monotonic()
136
+ remaining = deadline - now
137
+ if remaining <= 0:
138
+ elapsed = now - attempt_start
139
+ await cancel_response(f"timed out after {elapsed:.1f}s")
120
140
  return APIResponse(
121
141
  id=self.context.task_id,
122
142
  model_internal=self.context.model_name,
@@ -128,8 +148,9 @@ class APIRequestBase(ABC):
128
148
  content=None,
129
149
  usage=None,
130
150
  )
151
+
131
152
  # poll for the response
132
- await asyncio.sleep(5.0)
153
+ await asyncio.sleep(min(poll_interval, max(remaining, 0)))
133
154
  async with session.get(
134
155
  url=f"{self.url}/{response_id}",
135
156
  headers=self.request_header,
@@ -146,6 +167,8 @@ class APIRequestBase(ABC):
146
167
  return await self.handle_response(http_response)
147
168
 
148
169
  except Exception as e:
170
+ if response_id:
171
+ await cancel_response(f"errored: {type(e).__name__}")
149
172
  raise_if_modal_exception(e)
150
173
  tb = traceback.format_exc()
151
174
  print(tb)
lm_deluge/mock_openai.py CHANGED
@@ -41,6 +41,8 @@ try:
41
41
  from openai.types.chat.chat_completion import Choice as ChatCompletionChoice
42
42
  from openai.types.chat.chat_completion_chunk import (
43
43
  Choice as ChunkChoice,
44
+ )
45
+ from openai.types.chat.chat_completion_chunk import (
44
46
  ChoiceDelta,
45
47
  ChoiceDeltaToolCall,
46
48
  ChoiceDeltaToolCallFunction,
@@ -63,56 +65,61 @@ __all__ = [
63
65
  "RateLimitError",
64
66
  ]
65
67
 
66
- from lm_deluge.client import LLMClient
67
- from lm_deluge.prompt import Conversation, Message, Part, Text, ToolCall, ToolResult
68
+ from lm_deluge.client import LLMClient, _LLMClient
69
+ from lm_deluge.prompt import CachePattern, Conversation, Message, Text, ToolCall
70
+ from lm_deluge.tool import Tool
68
71
 
69
72
 
70
- def _messages_to_conversation(messages: list[dict[str, Any]]) -> Conversation:
71
- """Convert OpenAI messages format to lm-deluge Conversation."""
72
- conv_messages = []
73
-
74
- for msg in messages:
75
- role = msg["role"]
76
- content = msg.get("content")
77
- tool_calls = msg.get("tool_calls")
78
- tool_call_id = msg.get("tool_call_id")
79
-
80
- parts: list[Part] = []
81
-
82
- # Handle regular content
83
- if content:
84
- if isinstance(content, str):
85
- parts.append(Text(content))
86
- elif isinstance(content, list):
87
- # Multi-part content (text, images, etc.)
88
- for item in content:
89
- if item.get("type") == "text":
90
- parts.append(Text(item["text"]))
91
- # Could add image support here later
92
-
93
- # Handle tool calls (from assistant)
94
- if tool_calls:
95
- for tc in tool_calls:
96
- # Parse arguments from JSON string to dict
97
- args_str = tc["function"]["arguments"]
98
- args_dict = (
99
- json.loads(args_str) if isinstance(args_str, str) else args_str
100
- )
101
- parts.append(
102
- ToolCall(
103
- id=tc["id"],
104
- name=tc["function"]["name"],
105
- arguments=args_dict,
106
- )
107
- )
73
+ def _openai_tools_to_lm_deluge(tools: list[dict[str, Any]]) -> list[Tool]:
74
+ """
75
+ Convert OpenAI tool format to lm-deluge Tool objects.
76
+
77
+ OpenAI format:
78
+ {
79
+ "type": "function",
80
+ "function": {
81
+ "name": "get_weather",
82
+ "description": "Get weather",
83
+ "parameters": {
84
+ "type": "object",
85
+ "properties": {...},
86
+ "required": [...]
87
+ }
88
+ }
89
+ }
90
+
91
+ lm-deluge format:
92
+ Tool(
93
+ name="get_weather",
94
+ description="Get weather",
95
+ parameters={...properties...},
96
+ required=[...]
97
+ )
98
+ """
99
+ lm_tools = []
100
+ for tool in tools:
101
+ if tool.get("type") == "function":
102
+ func = tool["function"]
103
+ params_schema = func.get("parameters", {})
104
+
105
+ # Extract properties and required from the parameters schema
106
+ properties = params_schema.get("properties", {})
107
+ required = params_schema.get("required", [])
108
+
109
+ lm_tool = Tool(
110
+ name=func["name"],
111
+ description=func.get("description"),
112
+ parameters=properties if properties else None,
113
+ required=required,
114
+ )
115
+ lm_tools.append(lm_tool)
108
116
 
109
- # Handle tool results (from tool role)
110
- if role == "tool" and tool_call_id:
111
- parts.append(ToolResult(tool_call_id=tool_call_id, result=content or ""))
117
+ return lm_tools
112
118
 
113
- conv_messages.append(Message(role=role, parts=parts))
114
119
 
115
- return Conversation(messages=conv_messages)
120
+ def _messages_to_conversation(messages: list[dict[str, Any]]) -> Conversation:
121
+ """Convert OpenAI messages format to lm-deluge Conversation."""
122
+ return Conversation.from_openai_chat(messages)
116
123
 
117
124
 
118
125
  def _response_to_chat_completion(
@@ -346,7 +353,7 @@ class MockCompletions:
346
353
  ChatCompletion (non-streaming) or AsyncIterator[ChatCompletionChunk] (streaming)
347
354
  """
348
355
  # Get or create client for this model
349
- client = self._parent._get_or_create_client(model)
356
+ client: _LLMClient = self._parent._get_or_create_client(model)
350
357
 
351
358
  # Convert messages to Conversation
352
359
  conversation = _messages_to_conversation(messages)
@@ -377,26 +384,19 @@ class MockCompletions:
377
384
  # Convert tools if provided
378
385
  lm_tools = None
379
386
  if tools:
380
- # For now, just pass through - lm-deluge will handle the format
381
- lm_tools = tools
387
+ # Convert from OpenAI format to lm-deluge Tool objects
388
+ lm_tools = _openai_tools_to_lm_deluge(tools)
382
389
 
383
390
  # Execute request
384
391
  if stream:
385
- # Streaming mode
386
- request_id = f"chatcmpl-{uuid.uuid4().hex[:24]}"
387
- # Note: client.stream() is an async generator, not a coroutine
388
- # We can directly wrap it
389
- stream_iter = client.stream(conversation, tools=lm_tools)
390
- # Verify it's a generator, not a coroutine
391
- if hasattr(stream_iter, "__anext__"):
392
- return _AsyncStreamWrapper(stream_iter, model, request_id)
393
- else:
394
- # If it's a coroutine, we need to await it first
395
- # But this shouldn't happen with the current implementation
396
- raise TypeError(f"Expected async generator, got {type(stream_iter)}")
392
+ raise RuntimeError("streaming not supported")
397
393
  else:
398
394
  # Non-streaming mode
399
- response = await client.start(conversation, tools=lm_tools)
395
+ response = await client.start(
396
+ conversation,
397
+ tools=lm_tools, # type: ignore
398
+ cache=self._parent.cache_pattern, # type: ignore
399
+ )
400
400
  return _response_to_chat_completion(response, model)
401
401
 
402
402
 
@@ -437,7 +437,7 @@ class MockTextCompletions:
437
437
  Completion object
438
438
  """
439
439
  # Get or create client for this model
440
- client = self._parent._get_or_create_client(model)
440
+ client: _LLMClient = self._parent._get_or_create_client(model)
441
441
 
442
442
  # Handle single prompt
443
443
  if isinstance(prompt, list):
@@ -464,7 +464,7 @@ class MockTextCompletions:
464
464
  client = self._parent._create_client_with_params(model, merged_params)
465
465
 
466
466
  # Execute request
467
- response = await client.start(conversation)
467
+ response = await client.start(conversation, cache=self._parent.cache_pattern) # type: ignore
468
468
 
469
469
  # Convert to Completion format
470
470
  completion_text = None
@@ -477,7 +477,7 @@ class MockTextCompletions:
477
477
  choice = TextCompletionChoice(
478
478
  index=0,
479
479
  text=completion_text or "",
480
- finish_reason=response.finish_reason or "stop",
480
+ finish_reason=response.finish_reason or "stop", # type: ignore
481
481
  )
482
482
 
483
483
  # Create usage
@@ -560,6 +560,7 @@ class MockAsyncOpenAI:
560
560
  max_completion_tokens: int | None = None,
561
561
  top_p: float | None = None,
562
562
  seed: int | None = None,
563
+ cache_pattern: CachePattern | None = None,
563
564
  **kwargs: Any,
564
565
  ):
565
566
  # OpenAI-compatible attributes
@@ -571,6 +572,7 @@ class MockAsyncOpenAI:
571
572
  self.max_retries = max_retries or 2
572
573
  self.default_headers = default_headers
573
574
  self.http_client = http_client
575
+ self.cache_pattern = cache_pattern
574
576
 
575
577
  # Internal attributes
576
578
  self._default_model = model or "gpt-4o-mini"
lm_deluge/prompt.py CHANGED
@@ -848,14 +848,16 @@ class Conversation:
848
848
  if content is None:
849
849
  return parts
850
850
  if isinstance(content, str):
851
- parts.append(Text(content))
851
+ if content.strip():
852
+ parts.append(Text(content))
852
853
  return parts
853
854
 
854
855
  for block in content:
855
856
  block_type = block.get("type")
856
857
  if block_type in text_types:
857
858
  text_value = block.get("text") or block.get(block_type) or ""
858
- parts.append(Text(text_value))
859
+ if text_value.strip():
860
+ parts.append(Text(text_value))
859
861
  elif block_type in image_types:
860
862
  parts.append(_to_image_from_url(block))
861
863
  elif block_type in file_types:
@@ -1001,7 +1003,8 @@ class Conversation:
1001
1003
  )
1002
1004
  )
1003
1005
 
1004
- conversation_messages.append(Message(mapped_role, parts))
1006
+ if parts:
1007
+ conversation_messages.append(Message(mapped_role, parts))
1005
1008
 
1006
1009
  return cls(conversation_messages)
1007
1010
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lm_deluge
3
- Version: 0.0.70
3
+ Version: 0.0.72
4
4
  Summary: Python utility for using LLM API models.
5
5
  Author-email: Benjamin Anderson <ben@trytaylor.ai>
6
6
  Requires-Python: >=3.10
@@ -8,8 +8,8 @@ lm_deluge/embed.py,sha256=CO-TOlC5kOTAM8lcnicoG4u4K664vCBwHF1vHa-nAGg,13382
8
8
  lm_deluge/errors.py,sha256=oHjt7YnxWbh-eXMScIzov4NvpJMo0-2r5J6Wh5DQ1tk,209
9
9
  lm_deluge/file.py,sha256=PTmlJQ-IaYcYUFun9V0bJ1NPVP84edJrR0hvCMWFylY,19697
10
10
  lm_deluge/image.py,sha256=5AMXmn2x47yXeYNfMSMAOWcnlrOxxOel-4L8QCJwU70,8928
11
- lm_deluge/mock_openai.py,sha256=dYZDBKgTepQ-yd5zPRYBgMRXO6TeLqiM1fDQe622Ono,22110
12
- lm_deluge/prompt.py,sha256=Bgszws8-3GPefiVRa-Mht4tfyfoqD_hV5MX1nrbkJn0,63465
11
+ lm_deluge/mock_openai.py,sha256=-u4kxSzwoxDt_2fLh5LaiqETnu0Jg_VDL7TWAAYHGNw,21762
12
+ lm_deluge/prompt.py,sha256=b93ZZHlK9luujgilcnSkwoPCD-U6r1wLWXxWJ4D4ZIE,63578
13
13
  lm_deluge/request_context.py,sha256=cBayMFWupWhde2OjRugW3JH-Gin-WFGc6DK2Mb4Prdc,2576
14
14
  lm_deluge/rerank.py,sha256=-NBAJdHz9OB-SWWJnHzkFmeVO4wR6lFV7Vw-SxG7aVo,11457
15
15
  lm_deluge/tool.py,sha256=Kp2O5lDq_WVo_ASxjLQSHzVRbaxZkS6J0JIIskBjux0,28909
@@ -18,7 +18,7 @@ lm_deluge/usage.py,sha256=xz9tAw2hqaJvv9aAVhnQ6N1Arn7fS8Shb28VwCW26wI,5136
18
18
  lm_deluge/warnings.py,sha256=nlDJMCw30VhDEFxqLO2-bfXH_Tv5qmlglzUSbokCSw8,1498
19
19
  lm_deluge/api_requests/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
20
20
  lm_deluge/api_requests/anthropic.py,sha256=QGq3G5jJIGcoM2HdRt73GgkvZs4GOViyjYexWex05Vk,8927
21
- lm_deluge/api_requests/base.py,sha256=GCcydwBRx4_xAuYLvasXlyj-TgqvKAVhVvxRfJkvPbY,9471
21
+ lm_deluge/api_requests/base.py,sha256=mXEM85mcU_5LD-ugELpCl28tv-tpHKcaxerTIVLQZVo,10436
22
22
  lm_deluge/api_requests/bedrock.py,sha256=Uppne03GcIEk1tVYzoGu7GXK2Sg94a_xvFTLDRN_phY,15412
23
23
  lm_deluge/api_requests/chat_reasoning.py,sha256=sJvstvKFqsSBUjYcwxzGt2_FH4cEp3Z6gKcBPyPjGwk,236
24
24
  lm_deluge/api_requests/common.py,sha256=BZ3vRO5TB669_UsNKugkkuFSzoLHOYJIKt4nV4sf4vc,422
@@ -69,8 +69,8 @@ lm_deluge/util/logprobs.py,sha256=UkBZakOxWluaLqHrjARu7xnJ0uCHVfLGHJdnYlEcutk,11
69
69
  lm_deluge/util/spatial.py,sha256=BsF_UKhE-x0xBirc-bV1xSKZRTUhsOBdGqsMKme20C8,4099
70
70
  lm_deluge/util/validation.py,sha256=hz5dDb3ebvZrZhnaWxOxbNSVMI6nmaOODBkk0htAUhs,1575
71
71
  lm_deluge/util/xml.py,sha256=Ft4zajoYBJR3HHCt2oHwGfymGLdvp_gegVmJ-Wqk4Ck,10547
72
- lm_deluge-0.0.70.dist-info/licenses/LICENSE,sha256=uNNXGXPCw2TC7CUs7SEBkA-Mz6QBQFWUUEWDMgEs1dU,1058
73
- lm_deluge-0.0.70.dist-info/METADATA,sha256=URQWK2LB1itY_viE7mv0ijJOfUolZMDRzvK-Pdzmn_o,13514
74
- lm_deluge-0.0.70.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
75
- lm_deluge-0.0.70.dist-info/top_level.txt,sha256=hqU-TJX93yBwpgkDtYcXyLr3t7TLSCCZ_reytJjwBaE,10
76
- lm_deluge-0.0.70.dist-info/RECORD,,
72
+ lm_deluge-0.0.72.dist-info/licenses/LICENSE,sha256=uNNXGXPCw2TC7CUs7SEBkA-Mz6QBQFWUUEWDMgEs1dU,1058
73
+ lm_deluge-0.0.72.dist-info/METADATA,sha256=Ffg1w5rphPj_MScOCYhA1cQmSKsc2XjBqJefXiZOtDk,13514
74
+ lm_deluge-0.0.72.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
75
+ lm_deluge-0.0.72.dist-info/top_level.txt,sha256=hqU-TJX93yBwpgkDtYcXyLr3t7TLSCCZ_reytJjwBaE,10
76
+ lm_deluge-0.0.72.dist-info/RECORD,,