Skip to content

Commit 4a0ec6d

Browse files
committed
gemini multimodal support init
1 parent 3eb98e5 commit 4a0ec6d

File tree

7 files changed

+1279
-1541
lines changed

7 files changed

+1279
-1541
lines changed

cachy.jsonl

Lines changed: 98 additions & 0 deletions
Large diffs are not rendered by default.

lisette/_modidx.py

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,12 @@
1919
'lisette.core.Chat.__init__': ('core.html#chat.__init__', 'lisette/core.py'),
2020
'lisette.core.Chat._call': ('core.html#chat._call', 'lisette/core.py'),
2121
'lisette.core.Chat._prep_msg': ('core.html#chat._prep_msg', 'lisette/core.py'),
22-
'lisette.core.Chat.print_hist': ('core.html#chat.print_hist', 'lisette/core.py'),
2322
'lisette.core.ToolResponse': ('core.html#toolresponse', 'lisette/core.py'),
2423
'lisette.core._add_cache_control': ('core.html#_add_cache_control', 'lisette/core.py'),
2524
'lisette.core._alite_call_func': ('core.html#_alite_call_func', 'lisette/core.py'),
2625
'lisette.core._apply_cache_idxs': ('core.html#_apply_cache_idxs', 'lisette/core.py'),
2726
'lisette.core._bytes2content': ('core.html#_bytes2content', 'lisette/core.py'),
27+
'lisette.core._detect_mime': ('core.html#_detect_mime', 'lisette/core.py'),
2828
'lisette.core._extract_tool': ('core.html#_extract_tool', 'lisette/core.py'),
2929
'lisette.core._has_cache': ('core.html#_has_cache', 'lisette/core.py'),
3030
'lisette.core._has_search': ('core.html#_has_search', 'lisette/core.py'),
@@ -43,13 +43,8 @@
4343
'lisette/core.py'),
4444
'lisette.core.mk_msg': ('core.html#mk_msg', 'lisette/core.py'),
4545
'lisette.core.mk_msgs': ('core.html#mk_msgs', 'lisette/core.py'),
46-
'lisette.core.mk_tc': ('core.html#mk_tc', 'lisette/core.py'),
47-
'lisette.core.mk_tc_req': ('core.html#mk_tc_req', 'lisette/core.py'),
48-
'lisette.core.mk_tc_result': ('core.html#mk_tc_result', 'lisette/core.py'),
49-
'lisette.core.mk_tc_results': ('core.html#mk_tc_results', 'lisette/core.py'),
5046
'lisette.core.mk_tr_details': ('core.html#mk_tr_details', 'lisette/core.py'),
5147
'lisette.core.patch_litellm': ('core.html#patch_litellm', 'lisette/core.py'),
52-
'lisette.core.random_tool_id': ('core.html#random_tool_id', 'lisette/core.py'),
5348
'lisette.core.remove_cache_ckpts': ('core.html#remove_cache_ckpts', 'lisette/core.py'),
5449
'lisette.core.stream_with_complete': ('core.html#stream_with_complete', 'lisette/core.py')},
5550
'lisette.usage': { 'lisette.usage.LisetteUsageLogger': ('usage.html#lisetteusagelogger', 'lisette/usage.py'),

lisette/core.py

Lines changed: 33 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,7 @@
55
# %% auto 0
66
__all__ = ['sonn45', 'detls_tag', 're_tools', 'effort', 'patch_litellm', 'remove_cache_ckpts', 'contents', 'mk_msg', 'fmt2hist',
77
'mk_msgs', 'stream_with_complete', 'lite_mk_func', 'ToolResponse', 'cite_footnote', 'cite_footnotes', 'Chat',
8-
'random_tool_id', 'mk_tc', 'mk_tc_req', 'mk_tc_result', 'mk_tc_results', 'astream_with_complete',
9-
'AsyncChat', 'mk_tr_details', 'AsyncStreamFormatter', 'adisplay_stream']
8+
'astream_with_complete', 'AsyncChat', 'mk_tr_details', 'AsyncStreamFormatter', 'adisplay_stream']
109

1110
# %% ../nbs/00_core.ipynb
1211
import asyncio, base64, json, litellm, mimetypes, random, string
@@ -63,23 +62,37 @@ def _repr_markdown_(self: litellm.ModelResponse):
6362
</details>"""
6463

6564
# %% ../nbs/00_core.ipynb
66-
register_model({
67-
"claude-sonnet-4-5": {
68-
"max_tokens": 64000, "max_input_tokens": 200000, "max_output_tokens": 64000,
69-
"input_cost_per_token": 3e-06, "output_cost_per_token": 1.5e-05, "cache_creation_input_token_cost": 3.75e-06, "cache_read_input_token_cost": 3e-07,
70-
"litellm_provider": "anthropic", "mode": "chat",
71-
"supports_function_calling": True, "supports_parallel_function_calling": True, "supports_vision": True, "supports_prompt_caching": True, "supports_response_schema": True, "supports_system_messages": True, "supports_reasoning": True, "supports_assistant_prefill": True,
72-
"supports_tool_choice": True, "supports_computer_use": True
73-
}
74-
});
7565
sonn45 = "claude-sonnet-4-5"
7666

7767
# %% ../nbs/00_core.ipynb
68+
_sigs = {
69+
(b'%PDF', 0): 'application/pdf',
70+
(b'RIFF', 0): lambda d: 'audio/wav' if d[8:12]==b'WAVE' else 'video/avi' if d[8:12]==b'AVI ' else None,
71+
(b'ID3', 0): 'audio/mp3',
72+
(b'\xff\xfb', 0): 'audio/mp3',
73+
(b'\xff\xf3', 0): 'audio/mp3',
74+
(b'FORM', 0): lambda d: 'audio/aiff' if d[8:12]==b'AIFF' else None,
75+
(b'OggS', 0): 'audio/ogg',
76+
(b'fLaC', 0): 'audio/flac',
77+
(b'ftyp', 4): lambda d: 'video/3gpp' if d[8:11]==b'3gp' else 'video/mp4',
78+
(b'\x1a\x45\xdf', 0): 'video/webm',
79+
(b'FLV', 0): 'video/x-flv',
80+
(b'\x30\x26\xb2\x75', 0): 'video/wmv',
81+
(b'\x00\x00\x01\xb3', 0): 'video/mpeg',
82+
}
83+
84+
def _detect_mime(data):
85+
for (sig,pos),mime in _sigs.items():
86+
if data[pos:pos+len(sig)]==sig: return mime(data) if callable(mime) else mime
87+
return mimetypes.types_map.get(f'.{imghdr.what(None, h=data)}')
88+
7889
def _bytes2content(data):
79-
"Convert bytes to litellm content dict (image or pdf)"
80-
mtype = 'application/pdf' if data[:4] == b'%PDF' else mimetypes.types_map.get(f'.{imghdr.what(None, h=data)}')
81-
if not mtype: raise ValueError(f'Data must be image or PDF bytes, got {data[:10]}')
82-
return {'type': 'image_url', 'image_url': f'data:{mtype};base64,{base64.b64encode(data).decode("utf-8")}'}
90+
"Convert bytes to litellm content dict (image, pdf, audio, video)"
91+
mtype = _detect_mime(data)
92+
if not mtype: raise ValueError(f'Data must be a supported file type, got {data[:10]}')
93+
encoded = base64.b64encode(data).decode("utf-8")
94+
if mtype.startswith('image/'): return {'type': 'image_url', 'image_url': f'data:{mtype};base64,{encoded}'}
95+
return {'type': 'file', 'file': {'file_data': f'data:{mtype};base64,{encoded}'}}
8396

8497
# %% ../nbs/00_core.ipynb
8598
def _add_cache_control(msg, # LiteLLM formatted msg
@@ -267,7 +280,7 @@ def _prep_msg(self, msg=None, prefill=None):
267280
cache_idxs = L(self.cache_idxs).filter().map(lambda o: o-1 if o>0 else o)
268281
else:
269282
cache_idxs = self.cache_idxs
270-
if msg: self.hist = mk_msgs(self.hist+[msg], self.cache, cache_idxs, self.ttl)
283+
if msg: self.hist = mk_msgs(self.hist+[msg], self.cache and 'claude' in self.model, cache_idxs, self.ttl)
271284
pf = [{"role":"assistant","content":prefill}] if prefill else []
272285
return sp + self.hist + pf
273286

@@ -288,6 +301,7 @@ def _call(self, msg=None, prefill=None, temp=None, think=None, search=None, stre
288301
tools=self.tool_schemas, reasoning_effort = effort.get(think), tool_choice=tool_choice,
289302
# temperature is not supported when reasoning
290303
temperature=None if think else ifnone(temp,self.temp),
304+
caching=self.cache and 'claude' not in self.model,
291305
**kwargs)
292306
if stream:
293307
if prefill: yield _mk_prefill(prefill)
@@ -324,35 +338,6 @@ def __call__(self,
324338
elif return_all: return list(result_gen) # toolloop behavior
325339
else: return last(result_gen) # normal chat behavior
326340

327-
# %% ../nbs/00_core.ipynb
328-
@patch
329-
def print_hist(self:Chat):
330-
"Print each message on a different line"
331-
for r in self.hist: print(r, end='\n\n')
332-
333-
# %% ../nbs/00_core.ipynb
334-
def random_tool_id():
335-
"Generate a random tool ID with 'toolu_' prefix"
336-
random_part = ''.join(random.choices(string.ascii_letters + string.digits, k=25))
337-
return f'toolu_{random_part}'
338-
339-
# %% ../nbs/00_core.ipynb
340-
def mk_tc(func, args, tcid=None, idx=1):
341-
if not tcid: tcid = random_tool_id()
342-
return {'index': idx, 'function': {'arguments': args, 'name': func}, 'id': tcid, 'type': 'function'}
343-
344-
# %% ../nbs/00_core.ipynb
345-
def mk_tc_req(content, tcs):
346-
msg = Message(content=content, role='assistant', tool_calls=tcs, function_call=None)
347-
msg.tool_calls = [{**dict(tc), 'function': dict(tc['function'])} for tc in msg.tool_calls]
348-
return msg
349-
350-
# %% ../nbs/00_core.ipynb
351-
def mk_tc_result(tc, result): return {'tool_call_id': tc['id'], 'role': 'tool', 'name': tc['function']['name'], 'content': result}
352-
353-
# %% ../nbs/00_core.ipynb
354-
def mk_tc_results(tcq, results): return [mk_tc_result(a,b) for a,b in zip(tcq.tool_calls, results)]
355-
356341
# %% ../nbs/00_core.ipynb
357342
async def _alite_call_func(tc, ns, raise_on_err=True):
358343
try: fargs = json.loads(tc.function.arguments)
@@ -383,6 +368,7 @@ async def _call(self, msg=None, prefill=None, temp=None, think=None, search=None
383368
tools=self.tool_schemas, reasoning_effort=effort.get(think), tool_choice=tool_choice,
384369
# temperature is not supported when reasoning
385370
temperature=None if think else ifnone(temp,self.temp),
371+
caching=self.cache and 'claude' not in self.model,
386372
**kwargs)
387373
if stream:
388374
if prefill: yield _mk_prefill(prefill)
@@ -449,9 +435,9 @@ def format_item(self, o):
449435
res = ''
450436
if isinstance(o, ModelResponseStream):
451437
d = o.choices[0].delta
452-
if nested_idx(d, 'reasoning_content'):
438+
if nested_idx(d, 'reasoning_content') and d['reasoning_content']!='{"text": ""}':
453439
self.think = True
454-
res += '🧠'
440+
res += '🧠' if not self.outp or self.outp[-1]=='🧠' else '\n\n🧠'
455441
elif self.think:
456442
self.think = False
457443
res += '\n\n'

lisette/usage.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,17 @@ def log_success_event(self, kwargs, response_obj, start_time, end_time):
2727
def _log_usage(self, response_obj, response_cost, start_time, end_time):
2828
usage = response_obj.usage
2929
ptd = usage.prompt_tokens_details
30-
self.usage.insert(Usage(timestamp=time.time(), model=response_obj.model, user_id=self.user_id_fn(), prompt_tokens=usage.prompt_tokens, completion_tokens=usage.completion_tokens,
31-
total_tokens=usage.total_tokens, cached_tokens=ptd.cached_tokens if ptd else 0, cache_creation_tokens=usage.cache_creation_input_tokens,
32-
cache_read_tokens=usage.cache_read_input_tokens, web_search_requests=nested_idx(usage, 'server_tool_use', 'web_search_requests'), response_cost=response_cost))
30+
self.usage.insert(Usage(timestamp=time.time(),
31+
model=response_obj.model,
32+
user_id=self.user_id_fn(),
33+
prompt_tokens=usage.prompt_tokens,
34+
completion_tokens=usage.completion_tokens,
35+
total_tokens=usage.total_tokens,
36+
cached_tokens=ptd.cached_tokens if ptd else 0, # used by gemini (read tokens)
37+
cache_creation_tokens=nested_idx(usage, 'cache_creation_input_tokens'),
38+
cache_read_tokens=nested_idx(usage, 'cache_read_input_tokens'), # used by anthropic
39+
web_search_requests=nested_idx(usage, 'server_tool_use', 'web_search_requests'),
40+
response_cost=response_cost))
3341

3442
def user_id_fn(self): raise NotImplementedError('Please implement `LisetteUsageLogger.user_id_fn` before initializing, e.g using fastcore.patch.')
3543

0 commit comments

Comments
 (0)