python-fastllm 0.0.11__tar.gz → 0.0.14__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.11 → python_fastllm-0.0.14}/PKG-INFO +2 -1
  2. python_fastllm-0.0.14/fastllm/__init__.py +1 -0
  3. {python_fastllm-0.0.11 → python_fastllm-0.0.14}/fastllm/_modidx.py +4 -1
  4. {python_fastllm-0.0.11 → python_fastllm-0.0.14}/fastllm/acomplete.py +2 -1
  5. {python_fastllm-0.0.11 → python_fastllm-0.0.14}/fastllm/anthropic.py +10 -5
  6. {python_fastllm-0.0.11 → python_fastllm-0.0.14}/fastllm/chat.py +3 -3
  7. {python_fastllm-0.0.11 → python_fastllm-0.0.14}/fastllm/openai_responses.py +5 -2
  8. {python_fastllm-0.0.11 → python_fastllm-0.0.14}/fastllm/streaming.py +1 -5
  9. {python_fastllm-0.0.11 → python_fastllm-0.0.14}/fastllm/types.py +17 -4
  10. {python_fastllm-0.0.11 → python_fastllm-0.0.14}/pyproject.toml +1 -1
  11. {python_fastllm-0.0.11 → python_fastllm-0.0.14}/python_fastllm.egg-info/PKG-INFO +2 -1
  12. {python_fastllm-0.0.11 → python_fastllm-0.0.14}/python_fastllm.egg-info/requires.txt +1 -0
  13. python_fastllm-0.0.11/fastllm/__init__.py +0 -1
  14. {python_fastllm-0.0.11 → python_fastllm-0.0.14}/README.md +0 -0
  15. {python_fastllm-0.0.11 → python_fastllm-0.0.14}/fastllm/codex.py +0 -0
  16. {python_fastllm-0.0.11 → python_fastllm-0.0.14}/fastllm/gemini.py +0 -0
  17. {python_fastllm-0.0.11 → python_fastllm-0.0.14}/fastllm/openai_chat.py +0 -0
  18. {python_fastllm-0.0.11 → python_fastllm-0.0.14}/fastllm/specs/anthropic.json +0 -0
  19. {python_fastllm-0.0.11 → python_fastllm-0.0.14}/fastllm/specs/anthropic.yml +0 -0
  20. {python_fastllm-0.0.11 → python_fastllm-0.0.14}/fastllm/specs/gemini.json +0 -0
  21. {python_fastllm-0.0.11 → python_fastllm-0.0.14}/fastllm/specs/openai.with-code-samples.json +0 -0
  22. {python_fastllm-0.0.11 → python_fastllm-0.0.14}/fastllm/specs/openai.with-code-samples.yml +0 -0
  23. {python_fastllm-0.0.11 → python_fastllm-0.0.14}/fastllm/specs/spec_manifest.json +0 -0
  24. {python_fastllm-0.0.11 → python_fastllm-0.0.14}/python_fastllm.egg-info/SOURCES.txt +0 -0
  25. {python_fastllm-0.0.11 → python_fastllm-0.0.14}/python_fastllm.egg-info/dependency_links.txt +0 -0
  26. {python_fastllm-0.0.11 → python_fastllm-0.0.14}/python_fastllm.egg-info/entry_points.txt +0 -0
  27. {python_fastllm-0.0.11 → python_fastllm-0.0.14}/python_fastllm.egg-info/top_level.txt +0 -0
  28. {python_fastllm-0.0.11 → python_fastllm-0.0.14}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-fastllm
3
- Version: 0.0.11
3
+ Version: 0.0.14
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
@@ -11,6 +11,7 @@ Requires-Python: >=3.10
11
11
  Description-Content-Type: text/markdown
12
12
  Requires-Dist: fastspec
13
13
  Requires-Dist: toolslm
14
+ Requires-Dist: pillow
14
15
 
15
16
  # fastllm
16
17
 
@@ -0,0 +1 @@
1
+ __version__ = "0.0.14"
@@ -19,6 +19,7 @@ d = { 'settings': { 'branch': 'main',
19
19
  'fastllm.acomplete.mk_client': ('acomplete.html#mk_client', 'fastllm/acomplete.py')},
20
20
  'fastllm.anthropic': { 'fastllm.anthropic._ant_cc': ('anthropic.html#_ant_cc', 'fastllm/anthropic.py'),
21
21
  'fastllm.anthropic._ant_part_type': ('anthropic.html#_ant_part_type', 'fastllm/anthropic.py'),
22
+ 'fastllm.anthropic._sanid': ('anthropic.html#_sanid', 'fastllm/anthropic.py'),
22
23
  'fastllm.anthropic.acollect_stream': ('anthropic.html#acollect_stream', 'fastllm/anthropic.py'),
23
24
  'fastllm.anthropic.cost': ('anthropic.html#cost', 'fastllm/anthropic.py'),
24
25
  'fastllm.anthropic.delta_index_fn': ('anthropic.html#delta_index_fn', 'fastllm/anthropic.py'),
@@ -204,7 +205,8 @@ d = { 'settings': { 'branch': 'main',
204
205
  'fastllm.openai_chat.norm_parts': ('oai_chat.html#norm_parts', 'fastllm/openai_chat.py'),
205
206
  'fastllm.openai_chat.norm_sse_event': ('oai_chat.html#norm_sse_event', 'fastllm/openai_chat.py'),
206
207
  'fastllm.openai_chat.norm_tool_calls': ('oai_chat.html#norm_tool_calls', 'fastllm/openai_chat.py')},
207
- 'fastllm.openai_responses': { 'fastllm.openai_responses.acollect_stream': ( 'oai_responses.html#acollect_stream',
208
+ 'fastllm.openai_responses': { 'fastllm.openai_responses._sanid': ('oai_responses.html#_sanid', 'fastllm/openai_responses.py'),
209
+ 'fastllm.openai_responses.acollect_stream': ( 'oai_responses.html#acollect_stream',
208
210
  'fastllm/openai_responses.py'),
209
211
  'fastllm.openai_responses.cost': ('oai_responses.html#cost', 'fastllm/openai_responses.py'),
210
212
  'fastllm.openai_responses.delta_index_fn': ( 'oai_responses.html#delta_index_fn',
@@ -291,5 +293,6 @@ d = { 'settings': { 'branch': 'main',
291
293
  'fastllm.types.part_txt': ('types.html#part_txt', 'fastllm/types.py'),
292
294
  'fastllm.types.payload_kwargs': ('types.html#payload_kwargs', 'fastllm/types.py'),
293
295
  'fastllm.types.register_model_info': ('types.html#register_model_info', 'fastllm/types.py'),
296
+ 'fastllm.types.resize_b64': ('types.html#resize_b64', 'fastllm/types.py'),
294
297
  'fastllm.types.sys_text': ('types.html#sys_text', 'fastllm/types.py'),
295
298
  'fastllm.types.url_mime': ('types.html#url_mime', 'fastllm/types.py')}}}
@@ -29,7 +29,8 @@ oai_spec = SpecParser.from_openapi(dict2obj(json.loads((specs_path/'openai.with
29
29
  gem_spec = SpecParser.from_discovery(dict2obj(json.loads((specs_path/'gemini.json').read_text())))
30
30
 
31
31
  # %% ../nbs/06_acomplete.ipynb #32ee2546
32
- _codex_json = '~/.codex/auth.json', ('tokens','access_token')
32
+ _codex_path = os.getenv('CODEX_AUTH_PATH', '~/.codex/auth.json')
33
+ _codex_json = _codex_path, ('tokens','access_token')
33
34
  vendor_mapping = {
34
35
  "openai": ('openai', 'https://api.openai.com/v1', 'OPENAI_API_KEY'),
35
36
  "anthropic": ('anthropic', 'https://api.anthropic.com', 'ANTHROPIC_API_KEY'),
@@ -125,11 +125,14 @@ def _ant_cc(block, p):
125
125
  if (cc := (p.data or {}).get('cache_control')): block['cache_control'] = cc
126
126
  return block
127
127
 
128
+ # %% ../nbs/04_anthropic.ipynb #62e9a042
129
+ def _sanid(id_str): return re.sub(r'[^a-zA-Z0-9_-]', '_', id_str or '')
130
+
128
131
  # %% ../nbs/04_anthropic.ipynb #6ec772cb
129
132
  def denorm_tool_use(p:Part):
130
133
  "Convert canonical tool_use Part to Anthropic tool_use content block."
131
134
  d = p.data or {}
132
- block = dict(type='tool_use', id=d.get('id',''), name=d.get('name',''), input=d.get('arguments') or {})
135
+ block = dict(type='tool_use', id=_sanid(d.get('id','')), name=d.get('name',''), input=d.get('arguments') or {})
133
136
  if 'caller' in d: block['caller'] = d['caller']
134
137
  return _ant_cc(block, p)
135
138
 
@@ -238,8 +241,10 @@ def denorm_user(m:Msg):
238
241
  return dict(role='user', content=parts)
239
242
 
240
243
  # %% ../nbs/04_anthropic.ipynb #edd87272
241
- def denorm_image(p):
242
- if (b64:=data_url(p.text)): return {"type": "image", "source": {"type": "base64", "media_type": b64[0], "data": b64[1]}}
244
+ def denorm_image(p, max_sz=None):
245
+ if (b64:=data_url(p.text)):
246
+ data = resize_b64(b64[1], max_sz) if max_sz else b64[1]
247
+ return {"type": "image", "source": {"type": "base64", "media_type": b64[0], "data": data}}
243
248
  return {"type": "image", "source": {"type": "url", "url": p.text}}
244
249
 
245
250
  # %% ../nbs/04_anthropic.ipynb #fc6bbdfc
@@ -251,12 +256,12 @@ def denorm_file(p):
251
256
  def denorm_tool_result(p:Part):
252
257
  "Convert canonical tool_result Part to Anthropic tool_result content block."
253
258
  d = p.data or {}
254
- tid = d.get('id') or d.get('call_id','')
259
+ tid = _sanid(d.get('id') or d.get('call_id',''))
255
260
  if isinstance(p.text, list):
256
261
  blocks = []
257
262
  for pp in p.text:
258
263
  if pp.type == PartType.text: blocks.append({"type": "text", "text": pp.text or ""})
259
- elif pp.type == PartType.input_image: blocks.append(denorm_image(pp))
264
+ elif pp.type == PartType.input_image: blocks.append(denorm_image(pp, max_sz=2000))
260
265
  elif pp.type == PartType.input_file: blocks.append(denorm_file(pp))
261
266
  else: raise ValueError(f"Anthropic tool_result does not support {pp.type}")
262
267
  return _ant_cc(dict(type='tool_result', tool_use_id=tid, content=blocks), p)
@@ -183,7 +183,7 @@ def fmt2hist(outp:str)->list[Msg]:
183
183
  "Transform a formatted output string into fastllm canonical Msgs"
184
184
  if token_dtls_tag in outp: outp = re_token.sub('', outp)
185
185
  if tool_dtls_tag not in outp:
186
- msg = Msg(role='assistant', content=[Part(type=PartType.text, text=outp.strip())])
186
+ msg = Msg(role='assistant', content=[Part(type=PartType.text, text=outp.strip() or '.')])
187
187
  return _split_msg_on_fences(msg)
188
188
  hist, asst_parts, tool_parts = [], [], []
189
189
  def flush():
@@ -194,7 +194,7 @@ def fmt2hist(outp:str)->list[Msg]:
194
194
  for txt,_,tj in split_tools(outp):
195
195
  if txt and txt.strip():
196
196
  if tool_parts: flush()
197
- asst_parts.append(Part(type=PartType.text, text=txt.strip()))
197
+ asst_parts.append(Part(type=PartType.text, text=txt.strip() or '.'))
198
198
  if tj and (tp := _extract_tool_parts(tj)):
199
199
  asst_parts.append(tp[0])
200
200
  tool_parts.append(tp[1])
@@ -316,7 +316,7 @@ def _has_stop(tres_parts): return any(isinstance(p.text, StopResponse) for p in
316
316
  def _trunc_str(s, mx=2000, skip=10, replace="TRUNCATED"):
317
317
  "Truncate `s` to `mx` chars max, adding `replace` if truncated"
318
318
  if not isinstance(s, str): s = str(s)
319
- s = s.rstrip()
319
+ s = s.rstrip() if type(s) is str else type(s)(s.rstrip())
320
320
  if len(s)>2 and s[0]=='𝍁' and s[-1]=='𝍁':
321
321
  s = s[1:-1]
322
322
  if replace: return s
@@ -109,10 +109,13 @@ async def acollect_stream(resp, **kwargs):
109
109
  res = mk_acollect_stream(norm_and_yield(resp, norm_sse_event), index_fn=delta_index_fn, api_name='openai', **kwargs)
110
110
  async for o in res: yield o
111
111
 
112
+ # %% ../nbs/02_oai_responses.ipynb #9608e813
113
+ def _sanid(id_str): return id_str[:64] # codex max 64 char limit
114
+
112
115
  # %% ../nbs/02_oai_responses.ipynb #b746c82b
113
116
  def denorm_tool_use(p:Part):
114
117
  "Convert canonical tool_use Part back to OpenAI Responses function_call item."
115
- return dict(type='function_call', call_id=p.data.get('id'), name=p.data.get('name'), arguments=json.dumps(p.data.get('arguments', '{}')))
118
+ return dict(type='function_call', call_id=_sanid(p.data.get('id')), name=p.data.get('name'), arguments=json.dumps(p.data.get('arguments', '{}')))
116
119
 
117
120
  # %% ../nbs/02_oai_responses.ipynb #8f42adf7
118
121
  def denorm_tool(m:Msg):
@@ -208,7 +211,7 @@ def denorm_file(p):
208
211
  # %% ../nbs/02_oai_responses.ipynb #145b1c79
209
212
  def denorm_tool_result(m:Part):
210
213
  "Convert canonical tool result back to OpenAI Responses function_call_output item."
211
- cid = m.data.get('id', '') or m.data.get('call_id')
214
+ cid = _sanid(m.data.get('id', '') or m.data.get('call_id'))
212
215
  if isinstance(m.text, list):
213
216
  out = []
214
217
  for p in m.text:
@@ -137,14 +137,10 @@ async def mk_acollect_stream(it, index_fn, model=None, api_name=None, vendor_nam
137
137
  data = {**acc.extra, 'id':acc.id, 'name':acc.name, 'arguments':args, 'server':acc.server}
138
138
  yield Part(type=PartType.tool_use, data=data)
139
139
  # Server tool results for anthropic are yielded in d.server_tool_result by checking injected dummy `_delta`
140
- if acc.server and '_delta' not in tc.arguments: yield Part(type=PartType.tool_result, text="Server tool call executed.", data=data)
140
+ if acc.server: yield Part(type=PartType.tool_result, text="Server tool call executed.", data=data)
141
141
  if d.server_tool_result:
142
142
  idx = _fidx(d, 'server_tool_result')
143
143
  part_accum.parts[idx] = Part(type=typ, data=d.server_tool_result)
144
- srv_tc = next((p for p in reversed(list(part_accum.parts.values())) if isinstance(p, ToolCall) and p.server), None)
145
- if srv_tc:
146
- data = {**srv_tc.extra, 'id':srv_tc.id, 'name':srv_tc.name, 'arguments':srv_tc.arguments, 'server':True}
147
- yield Part(type=PartType.tool_result, text="Server tool call executed.", data=data)
148
144
  r = _proc(d, 'refusal')
149
145
  if r[0]: yield r[0]
150
146
  if d.finish_reason: fin = d.finish_reason
@@ -7,15 +7,16 @@ __all__ = ['PartType', 'FinishReason', 'api_registry', 'model_prices_url', 'haik
7
7
  'gpt54', 'gpt54m', 'gpt55', 'codex54', 'codex54m', 'codex55', 'codex53spark', 'model_info_registry',
8
8
  'modern_llm', 'deepseek_v4_common', 'mimo_v25_common', 'codex_pricing', 'Part', 'Msg', 'ToolCall',
9
9
  'display_list', 'Usage', 'Completion', 'APIRegistry', 'mk_completion', 'mk_tool_res_msg', 'fn_schema',
10
- 'sys_text', 'part_txt', 'data_url', 'url_mime', 'payload_kwargs', 'get_api_key', 'model_prices_meta',
11
- 'infer_api_name', 'get_model_meta', 'register_model_info', 'get_model_info', 'get_model_pricing',
12
- 'approx_pricing']
10
+ 'sys_text', 'part_txt', 'data_url', 'url_mime', 'payload_kwargs', 'get_api_key', 'resize_b64',
11
+ 'model_prices_meta', 'infer_api_name', 'get_model_meta', 'register_model_info', 'get_model_info',
12
+ 'get_model_pricing', 'approx_pricing']
13
13
 
14
14
  # %% ../nbs/00_types.ipynb #b4d047fd
15
- import httpx
15
+ import httpx, base64, io
16
16
  from dataclasses import dataclass, field
17
17
  from fastcore.net import urljson
18
18
  from fastcore.utils import *
19
+ from PIL import Image as PImg
19
20
 
20
21
  # %% ../nbs/00_types.ipynb #e568bade
21
22
  @dataclass
@@ -242,6 +243,18 @@ def get_api_key(api_key, default):
242
243
  if not key: raise ValueError(f"Missing API key: set environment variable '{default}' or pass `api_key` parameter")
243
244
  return key
244
245
 
246
+ # %% ../nbs/00_types.ipynb #25e9cd60
247
+ def resize_b64(b64, max_sz):
248
+ "Resize a base64 image data to a max long edge, preserving aspect ratio."
249
+ img = PImg.open(io.BytesIO(base64.b64decode(b64)))
250
+ if max(img.size) < max_sz: return b64
251
+ img.thumbnail((max_sz,max_sz), PImg.Resampling.LANCZOS)
252
+ fmt = (img.format or 'PNG').upper()
253
+ if fmt=='JPG': fmt = 'JPEG'
254
+ buf = io.BytesIO()
255
+ img.save(buf, format=fmt)
256
+ return base64.b64encode(buf.getvalue()).decode()
257
+
245
258
  # %% ../nbs/00_types.ipynb #852adecd
246
259
  model_prices_url = 'https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json'
247
260
 
@@ -15,7 +15,7 @@ classifiers = [
15
15
  "Programming Language :: Python :: 3",
16
16
  "Programming Language :: Python :: 3 :: Only",
17
17
  ]
18
- dependencies = ['fastspec', 'toolslm']
18
+ dependencies = ['fastspec', 'toolslm', 'pillow']
19
19
 
20
20
  [project.urls]
21
21
  Repository = "https://github.com/AnswerDotAI/fastllm"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-fastllm
3
- Version: 0.0.11
3
+ Version: 0.0.14
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
@@ -11,6 +11,7 @@ Requires-Python: >=3.10
11
11
  Description-Content-Type: text/markdown
12
12
  Requires-Dist: fastspec
13
13
  Requires-Dist: toolslm
14
+ Requires-Dist: pillow
14
15
 
15
16
  # fastllm
16
17
 
@@ -1 +0,0 @@
1
- __version__ = "0.0.11"