python-fastllm 0.0.23__tar.gz → 0.0.25__tar.gz

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.
Files changed (28) hide show
  1. {python_fastllm-0.0.23 → python_fastllm-0.0.25}/PKG-INFO +1 -1
  2. python_fastllm-0.0.25/fastllm/__init__.py +1 -0
  3. {python_fastllm-0.0.23 → python_fastllm-0.0.25}/fastllm/_modidx.py +1 -0
  4. {python_fastllm-0.0.23 → python_fastllm-0.0.25}/fastllm/acomplete.py +1 -1
  5. {python_fastllm-0.0.23 → python_fastllm-0.0.25}/fastllm/chat.py +28 -17
  6. {python_fastllm-0.0.23 → python_fastllm-0.0.25}/fastllm/types.py +20 -3
  7. {python_fastllm-0.0.23 → python_fastllm-0.0.25}/python_fastllm.egg-info/PKG-INFO +1 -1
  8. python_fastllm-0.0.23/fastllm/__init__.py +0 -1
  9. {python_fastllm-0.0.23 → python_fastllm-0.0.25}/README.md +0 -0
  10. {python_fastllm-0.0.23 → python_fastllm-0.0.25}/fastllm/anthropic.py +0 -0
  11. {python_fastllm-0.0.23 → python_fastllm-0.0.25}/fastllm/codex.py +0 -0
  12. {python_fastllm-0.0.23 → python_fastllm-0.0.25}/fastllm/gemini.py +0 -0
  13. {python_fastllm-0.0.23 → python_fastllm-0.0.25}/fastllm/openai_chat.py +0 -0
  14. {python_fastllm-0.0.23 → python_fastllm-0.0.25}/fastllm/openai_responses.py +0 -0
  15. {python_fastllm-0.0.23 → python_fastllm-0.0.25}/fastllm/specs/anthropic.json +0 -0
  16. {python_fastllm-0.0.23 → python_fastllm-0.0.25}/fastllm/specs/anthropic.yml +0 -0
  17. {python_fastllm-0.0.23 → python_fastllm-0.0.25}/fastllm/specs/gemini.json +0 -0
  18. {python_fastllm-0.0.23 → python_fastllm-0.0.25}/fastllm/specs/openai.with-code-samples.json +0 -0
  19. {python_fastllm-0.0.23 → python_fastllm-0.0.25}/fastllm/specs/openai.with-code-samples.yml +0 -0
  20. {python_fastllm-0.0.23 → python_fastllm-0.0.25}/fastllm/specs/spec_manifest.json +0 -0
  21. {python_fastllm-0.0.23 → python_fastllm-0.0.25}/fastllm/streaming.py +0 -0
  22. {python_fastllm-0.0.23 → python_fastllm-0.0.25}/pyproject.toml +0 -0
  23. {python_fastllm-0.0.23 → python_fastllm-0.0.25}/python_fastllm.egg-info/SOURCES.txt +0 -0
  24. {python_fastllm-0.0.23 → python_fastllm-0.0.25}/python_fastllm.egg-info/dependency_links.txt +0 -0
  25. {python_fastllm-0.0.23 → python_fastllm-0.0.25}/python_fastllm.egg-info/entry_points.txt +0 -0
  26. {python_fastllm-0.0.23 → python_fastllm-0.0.25}/python_fastllm.egg-info/requires.txt +0 -0
  27. {python_fastllm-0.0.23 → python_fastllm-0.0.25}/python_fastllm.egg-info/top_level.txt +0 -0
  28. {python_fastllm-0.0.23 → python_fastllm-0.0.25}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-fastllm
3
- Version: 0.0.23
3
+ Version: 0.0.25
4
4
  Author-email: Kerem Turgutlu <keremturgutlu@gmail.com>
5
5
  License: Apache-2.0
6
6
  Project-URL: Repository, https://github.com/AnswerDotAI/fastllm
@@ -0,0 +1 @@
1
+ __version__ = "0.0.25"
@@ -287,6 +287,7 @@ d = { 'settings': { 'branch': 'main',
287
287
  'fastllm.types.get_model_meta': ('types.html#get_model_meta', 'fastllm/types.py'),
288
288
  'fastllm.types.get_model_pricing': ('types.html#get_model_pricing', 'fastllm/types.py'),
289
289
  'fastllm.types.infer_api_name': ('types.html#infer_api_name', 'fastllm/types.py'),
290
+ 'fastllm.types.is_deepseek_peak_hour': ('types.html#is_deepseek_peak_hour', 'fastllm/types.py'),
290
291
  'fastllm.types.mk_completion': ('types.html#mk_completion', 'fastllm/types.py'),
291
292
  'fastllm.types.mk_tool_res_msg': ('types.html#mk_tool_res_msg', 'fastllm/types.py'),
292
293
  'fastllm.types.model_prices_meta': ('types.html#model_prices_meta', 'fastllm/types.py'),
@@ -149,7 +149,7 @@ async def acomplete(msgs, model, api_name=None, vendor_name=None, api_key=None,
149
149
  api = api_registry.apis[api_name]
150
150
  payload = api.mk_payload(msgs, model, stream=stream, **kwargs)
151
151
  async def _mk_gen():
152
- async for o in api.acollect_stream(payload, model=model, vendor_name=vendor_name, stop_callables=stop_callables): yield o
152
+ async for o in api.acollect_stream(payload, model=model, vendor_name='claude_code', stop_callables=stop_callables): yield o
153
153
  return _retry_stream(_mk_gen, retries, retry_delay)
154
154
  cli, api_name, vendor_name = mk_client(model, vendor_name, api_name, api_key, base_url, xtra_hdrs)
155
155
  api = api_registry.apis[api_name]
@@ -3,13 +3,13 @@
3
3
  # AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/07_chat.ipynb.
4
4
 
5
5
  # %% auto #0
6
- __all__ = ['tool_dtls_tag', 're_tools', 'token_dtls_tag', 're_token', 'effort', 'MediaUrl', 'remove_cache_ckpts', 'contents',
7
- 'stop_reason', 'mk_msg', 'FenceToolStop', 'extract_fence_call', 'split_tools', 'fmt2hist', 'mk_msgs',
8
- 'cite_footnote', 'postproc', 'lite_mk_func', 'ToolResponse', 'structured', 'StopResponse', 'FullResponse',
9
- 'search_count', 'UsageStats', 'AsyncChat', 'astream_with_complete', 'ChatCallback', 'DeepseekMsgsCallback',
10
- 'DeepseekPrefillCallback', 'add_warning', 'StopReasonCallback', 'run_fence_tool', 'FenceToolCallback',
11
- 'ToolReminderCallback', 'stop_sequences', 'StopSequencesCallback', 'mk_tr_details', 'StreamFormatter',
12
- 'AsyncStreamFormatter', 'adisplay_stream']
6
+ __all__ = ['tool_dtls_tag', 're_tools', 'token_dtls_tag', 're_token', 'think_start', 'think_end', 're_think', 'effort',
7
+ 'MediaUrl', 'remove_cache_ckpts', 'contents', 'stop_reason', 'mk_msg', 'FenceToolStop', 'extract_fence_call',
8
+ 'split_tools', 'fmt2hist', 'mk_msgs', 'cite_footnote', 'postproc', 'lite_mk_func', 'ToolResponse',
9
+ 'structured', 'StopResponse', 'FullResponse', 'search_count', 'UsageStats', 'AsyncChat',
10
+ 'astream_with_complete', 'ChatCallback', 'DeepseekMsgsCallback', 'DeepseekPrefillCallback', 'add_warning',
11
+ 'StopReasonCallback', 'run_fence_tool', 'FenceToolCallback', 'ToolReminderCallback', 'stop_sequences',
12
+ 'StopSequencesCallback', 'mk_tr_details', 'StreamFormatter', 'AsyncStreamFormatter', 'adisplay_stream']
13
13
 
14
14
  # %% ../nbs/07_chat.ipynb #d5a3bc1f
15
15
  import asyncio, base64, json, mimetypes, random, string, ast, warnings
@@ -111,6 +111,8 @@ re_tools = re.compile(fr"^({tool_dtls_tag}\n*(?:<summary>(?P<summary>.*?)</summa
111
111
  token_dtls_tag = "<details class='token-usage-details' markdown='1'>"
112
112
  re_token = re.compile(fr"^{re.escape(token_dtls_tag)}\n*<summary>.*?</summary>\n*\n*`.*?`\n*\n*</details>\n?",
113
113
  flags=re.DOTALL|re.MULTILINE)
114
+ think_start,think_end = '<!--think_start-->','<!--think_end-->'
115
+ re_think = re.compile(rf'{re.escape(think_start)}.*?{re.escape(think_end)}\n?', re.DOTALL)
114
116
 
115
117
  # %% ../nbs/07_chat.ipynb #be998131
116
118
  _fence_back = '`````'
@@ -182,6 +184,7 @@ def split_tools(s):
182
184
  def fmt2hist(outp:str)->list[Msg]:
183
185
  "Transform a formatted output string into fastllm canonical Msgs"
184
186
  if token_dtls_tag in outp: outp = re_token.sub('', outp)
187
+ if think_start in outp: outp = re_think.sub('', outp)
185
188
  if tool_dtls_tag not in outp:
186
189
  msg = Msg(role='assistant', content=[Part(type=PartType.text, text=outp.strip() or '.')])
187
190
  return _split_msg_on_fences(msg)
@@ -345,13 +348,13 @@ def search_count(r):
345
348
 
346
349
  # %% ../nbs/07_chat.ipynb #61395e0d
347
350
  class UsageStats:
348
- def __init__(self, prompt_tokens=0, completion_tokens=0, total_tokens=0, cached_tokens=0, cache_creation_tokens=0, reasoning_tokens=0, web_search_requests=0, cost=0.0): store_attr()
351
+ def __init__(self, model='', prompt_tokens=0, completion_tokens=0, total_tokens=0, cached_tokens=0, cache_creation_tokens=0, reasoning_tokens=0, web_search_requests=0, cost=0.0): store_attr()
349
352
 
350
353
  @classmethod
351
354
  def from_response(cls, r):
352
355
  u = r.usage
353
356
  return cls(
354
- prompt_tokens=u.prompt_tokens or 0, completion_tokens=u.completion_tokens or 0, total_tokens=u.total_tokens or 0,
357
+ model=r.model or '', prompt_tokens=u.prompt_tokens or 0, completion_tokens=u.completion_tokens or 0, total_tokens=u.total_tokens or 0,
355
358
  cached_tokens=u.cached_tokens or 0, cache_creation_tokens=u.cache_creation_tokens or 0, reasoning_tokens=u.reasoning_tokens or 0,
356
359
  web_search_requests=search_count(r), cost=r.cost)
357
360
 
@@ -359,7 +362,7 @@ class UsageStats:
359
362
  if other is None: return self
360
363
  return UsageStats(**{k: getattr(self, k, 0) + getattr(other, k, 0)
361
364
  for k in ('prompt_tokens', 'completion_tokens', 'total_tokens', 'cached_tokens', 'cache_creation_tokens', 'reasoning_tokens', 'web_search_requests', 'cost')
362
- })
365
+ }, model=other.model or self.model)
363
366
  def __radd__(self, other): return self if other is None or other == 0 else self.__add__(other)
364
367
 
365
368
  def __repr__(self):
@@ -369,6 +372,7 @@ class UsageStats:
369
372
  if self.reasoning_tokens: parts.append(f"reasoning={self.reasoning_tokens:,}")
370
373
  if getattr(self, 'web_search_requests', None): parts.append(f"searches={self.web_search_requests}")
371
374
  if self.cost: parts.append(f"${self.cost:.4f}")
375
+ if self.model: parts.append(self.model.split('/')[-1])
372
376
  return ' | '.join(parts)
373
377
 
374
378
  def fmt(self):
@@ -740,35 +744,42 @@ def mk_tr_details(tr, mx=2000):
740
744
  # %% ../nbs/07_chat.ipynb #f0d984ec
741
745
  class StreamFormatter:
742
746
  def __init__(self, mx=2000, debug=False, showthink=False):
743
- self.outp,self.tcs = '',{}
747
+ self.outp,self.tcs,self._in_think = '',{},False
744
748
  store_attr()
745
749
 
746
750
  def format_item(self, o):
747
751
  "Format a single item from the response stream."
748
752
  res = ''
749
753
  if self.debug: print(o)
754
+ is_think = isinstance(o, dict) and o.get('thinking')
755
+ if not is_think and self._in_think: res += f'\n\n</details>\n{think_end}\n\n'; self._in_think = False
750
756
  if isinstance(o, dict):
751
757
  if thk:=o.get('thinking'):
752
- if self.showthink: res += thk
753
- res+= '🧠' if not self.outp or self.outp[-1]=='🧠' else '\n\n🧠'
758
+ if self.showthink:
759
+ if not self._in_think:
760
+ res += f'\n{think_start}\n<details><summary>Thinking</summary>\n\n'
761
+ self._in_think = True
762
+ res += thk
763
+ else: res+= '🧠' if not self.outp or self.outp[-1]=='🧠' else '\n\n🧠'
754
764
  elif self.outp and self.outp[-1] == '🧠': res+= '\n\n'
755
765
  if txt:=o.get('text'): res+=f"\n\n{txt}" if res and res[-1] == '🧠' else txt
756
- if isinstance(o, Part) and o.type==PartType.tool_use:
757
- res += f"\n- ⏳ {_tc_summary(o)} ⏳\n"
758
- if isinstance(o, Part) and o.type == PartType.tool_result:
759
- res += mk_tr_details(o,mx=self.mx)
766
+ if isinstance(o, Part):
767
+ if o.type==PartType.tool_use: res += f"\n- ⏳ {_tc_summary(o)} ⏳\n"
768
+ elif o.type==PartType.tool_result: res += mk_tr_details(o,mx=self.mx)
760
769
  self.outp+=res
761
770
  return res
762
771
 
763
772
  def format_stream(self, rs):
764
773
  "Format the response stream for markdown display."
765
774
  for o in rs: yield self.format_item(o)
775
+ if self._in_think: yield f'\n\n</details>\n{think_end}\n\n'
766
776
 
767
777
  # %% ../nbs/07_chat.ipynb #0cdd4d7c
768
778
  class AsyncStreamFormatter(StreamFormatter):
769
779
  async def format_stream(self, rs):
770
780
  "Format the response stream for markdown display."
771
781
  async for o in rs: yield self.format_item(o)
782
+ if self._in_think: yield f'\n\n</details>\n{think_end}\n\n'
772
783
 
773
784
  # %% ../nbs/07_chat.ipynb #944bcd25
774
785
  @delegates(AsyncStreamFormatter)
@@ -9,11 +9,12 @@ __all__ = ['PartType', 'FinishReason', 'api_registry', 'model_prices_url', 'haik
9
9
  'display_list', 'Usage', 'Completion', 'APIRegistry', 'mk_completion', 'mk_tool_res_msg', 'fn_schema',
10
10
  'sys_text', 'part_txt', 'data_url', 'url_mime', 'payload_kwargs', 'get_api_key', 'resize_b64',
11
11
  'model_prices_meta', 'infer_api_name', 'get_model_meta', 'register_model_info', 'get_model_info',
12
- 'get_model_pricing', 'approx_pricing']
12
+ 'get_model_pricing', 'approx_pricing', 'is_deepseek_peak_hour']
13
13
 
14
14
  # %% ../nbs/00_types.ipynb #b4d047fd
15
15
  import httpx, base64, io
16
16
  from dataclasses import dataclass, field
17
+ from datetime import datetime, timezone
17
18
  from fastcore.net import urljson
18
19
  from fastcore.utils import *
19
20
  from PIL import Image as PImg
@@ -331,7 +332,11 @@ register_model_info('accounts/fireworks/models/qwen3p6-plus', vendor_name='firew
331
332
 
332
333
  register_model_info('accounts/fireworks/models/glm-5p2', vendor_name='fireworks_ai', **modern_llm, supports_vision=False,
333
334
  max_tokens=1000000, max_input_tokens=1000000, max_output_tokens=65536,
334
- input_cost_per_token=1.4e-6, cache_read_input_token_cost=0.26e-6, output_cost_per_token=4.4e-6)
335
+ input_cost_per_token=1.4e-6, cache_read_input_token_cost=0.14e-6, output_cost_per_token=4.4e-6)
336
+
337
+ register_model_info('accounts/fireworks/routers/glm-5p2-fast', vendor_name='fireworks_ai', **modern_llm, supports_vision=False,
338
+ max_tokens=1000000, max_input_tokens=1000000, max_output_tokens=65536,
339
+ input_cost_per_token=2.8e-6, cache_read_input_token_cost=0.28e-6, output_cost_per_token=8.8e-6)
335
340
 
336
341
  register_model_info('gemini-3.5-flash', vendor_name='gemini', base='gemini-3-flash-preview',
337
342
  input_cost_per_token=1.5e-6, output_cost_per_token=9e-6,
@@ -397,6 +402,10 @@ register_model_info(codex53spark, 'codex', **codex_pricing,
397
402
  supports_vision=False, supports_image_input=False, supports_web_search=True, supports_reasoning=True, supports_function_calling=True,
398
403
  max_tokens=128000, max_input_tokens=128000, max_output_tokens=128000)
399
404
 
405
+ # %% ../nbs/00_types.ipynb #5d3e4720
406
+ for model in (haik45, sonn45, sonn46, opus46, opus):
407
+ register_model_info(model, 'claude_code', base=model, base_vendor_name='anthropic', **codex_pricing)
408
+
400
409
  # %% ../nbs/00_types.ipynb #24cc47ec
401
410
  def get_model_pricing(mn, vendor_name, million=True):
402
411
  return {k:round(v * (1e6 if million else 1), 6)
@@ -412,10 +421,18 @@ def approx_pricing(nm, vendor_name, out=10, cache=80, inp=10, markup=0):
412
421
  if nm in ('claude-opus-4-7','claude-opus-4-8','claude-fable-5'): res *= 1.5
413
422
  return res*(1+markup)
414
423
 
424
+ # %% ../nbs/00_types.ipynb #d2a310fb
425
+ def is_deepseek_peak_hour(dt=None):
426
+ "Check whether current UTC time is in DeepSeek peak pricing hours."
427
+ dt = dt or datetime.now(timezone.utc)
428
+ h = dt.hour + dt.minute/60
429
+ return 1 <= h < 4 or 6 <= h < 10
430
+
415
431
  # %% ../nbs/00_types.ipynb #8bfca02d
416
432
  @patch(as_prop=True)
417
433
  def cost(self:Completion):
418
434
  meta = dict2obj(get_model_info(self.model, self.vendor_name))
419
435
  api = api_registry.apis[self.api_name]
420
436
  if not hasattr(api, 'cost'): raise NotImplementedError(f"API: {self.api_name} doesn't have a registered `cost` function in ns")
421
- return api.cost(self.usage, meta)
437
+ res = api.cost(self.usage, meta)
438
+ return res*2 if self.vendor_name=='deepseek' and self.model.startswith('deepseek-v4') and is_deepseek_peak_hour() else res
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-fastllm
3
- Version: 0.0.23
3
+ Version: 0.0.25
4
4
  Author-email: Kerem Turgutlu <keremturgutlu@gmail.com>
5
5
  License: Apache-2.0
6
6
  Project-URL: Repository, https://github.com/AnswerDotAI/fastllm
@@ -1 +0,0 @@
1
- __version__ = "0.0.23"