python-fastllm 0.0.59__py3-none-any.whl → 0.0.61__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.
fastllm/__init__.py CHANGED
@@ -2,4 +2,4 @@
2
2
 
3
3
  - `fastllm.responses`: Provider-independent Responses resources and streaming over FastLLM"""
4
4
 
5
- __version__ = "0.0.59"
5
+ __version__ = "0.0.61"
fastllm/_modidx.py CHANGED
@@ -22,6 +22,8 @@ d = { 'settings': { 'branch': 'main',
22
22
  'fastllm.anthropic': { 'fastllm.anthropic._add_cache_control': ('anthropic.html#_add_cache_control', 'fastllm/anthropic.py'),
23
23
  'fastllm.anthropic._ant_cc': ('anthropic.html#_ant_cc', 'fastllm/anthropic.py'),
24
24
  'fastllm.anthropic._ant_part_type': ('anthropic.html#_ant_part_type', 'fastllm/anthropic.py'),
25
+ 'fastllm.anthropic._clear_cache_control': ( 'anthropic.html#_clear_cache_control',
26
+ 'fastllm/anthropic.py'),
25
27
  'fastllm.anthropic._sanid': ('anthropic.html#_sanid', 'fastllm/anthropic.py'),
26
28
  'fastllm.anthropic._url_cite': ('anthropic.html#_url_cite', 'fastllm/anthropic.py'),
27
29
  'fastllm.anthropic.acollect_stream': ('anthropic.html#acollect_stream', 'fastllm/anthropic.py'),
@@ -140,7 +142,8 @@ d = { 'settings': { 'branch': 'main',
140
142
  'fastllm.codex.main': ('codex.html#main', 'fastllm/codex.py'),
141
143
  'fastllm.codex.mk_jwt': ('codex.html#mk_jwt', 'fastllm/codex.py'),
142
144
  'fastllm.codex.token_expired': ('codex.html#token_expired', 'fastllm/codex.py')},
143
- 'fastllm.gemini': { 'fastllm.gemini._fn_calls': ('gemini.html#_fn_calls', 'fastllm/gemini.py'),
145
+ 'fastllm.gemini': { 'fastllm.gemini._fn_call': ('gemini.html#_fn_call', 'fastllm/gemini.py'),
146
+ 'fastllm.gemini._fn_calls': ('gemini.html#_fn_calls', 'fastllm/gemini.py'),
144
147
  'fastllm.gemini._gem_filter_sch': ('gemini.html#_gem_filter_sch', 'fastllm/gemini.py'),
145
148
  'fastllm.gemini._gem_part_type': ('gemini.html#_gem_part_type', 'fastllm/gemini.py'),
146
149
  'fastllm.gemini._url_cite': ('gemini.html#_url_cite', 'fastllm/gemini.py'),
@@ -315,7 +318,9 @@ d = { 'settings': { 'branch': 'main',
315
318
  'fastllm.responses._msg_parts': ('responses.html#_msg_parts', 'fastllm/responses.py'),
316
319
  'fastllm.responses._new_id': ('responses.html#_new_id', 'fastllm/responses.py'),
317
320
  'fastllm.responses._response_base': ('responses.html#_response_base', 'fastllm/responses.py'),
321
+ 'fastllm.responses._server_call': ('responses.html#_server_call', 'fastllm/responses.py'),
318
322
  'fastllm.responses._text_content': ('responses.html#_text_content', 'fastllm/responses.py'),
323
+ 'fastllm.responses._visible_parts': ('responses.html#_visible_parts', 'fastllm/responses.py'),
319
324
  'fastllm.responses.normalize_call_ids': ('responses.html#normalize_call_ids', 'fastllm/responses.py'),
320
325
  'fastllm.responses.pending_response': ('responses.html#pending_response', 'fastllm/responses.py'),
321
326
  'fastllm.responses.response_event': ('responses.html#response_event', 'fastllm/responses.py'),
fastllm/acomplete.py CHANGED
@@ -1,4 +1,4 @@
1
- """A high level unified function make api calls"""
1
+ """A unified async API for model completions and streaming"""
2
2
 
3
3
  # AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/06_acomplete.ipynb.
4
4
 
@@ -33,6 +33,7 @@ vendor_mapping = {
33
33
  "codex": ('openai', 'https://chatgpt.com/backend-api/codex', 'CODEX_AUTH_TOKEN', _codex_json),
34
34
  "moonshot": ('openai_chat', "https://api.moonshot.ai/v1", "MOONSHOT_API_KEY"),
35
35
  "deepseek": ('openai_chat', "https://api.deepseek.com/v1", "DEEPSEEK_API_KEY"),
36
+ "inception": ('openai_chat', "https://api.inceptionlabs.ai/v1", "INCEPTION_API_KEY"),
36
37
  "mimo": ('anthropic', "https://api.xiaomimimo.com/anthropic", "MIMO_API_KEY"),
37
38
  "openrouter": ('openai_chat', "https://openrouter.ai/api/v1", "OPENROUTER_API_KEY"),
38
39
  "together": ('openai_chat', "https://api.together.xyz/v1", "TOGETHER_API_KEY"),
fastllm/anthropic.py CHANGED
@@ -1,3 +1,5 @@
1
+ """Translate Anthropic Messages API requests, responses, and streams to and from fastllm's shared message types, with tool replay, media, and prompt caching"""
2
+
1
3
  # AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/04_anthropic.ipynb.
2
4
 
3
5
  # %% auto #0
@@ -16,7 +18,7 @@ from fasttransport.errors import api_error_from_event
16
18
 
17
19
  from .types import *
18
20
  from aidialog.msg_parts import (MdStr, Part, PartType, mk_part, Msg, Completion, Text, Thinking, ToolUse, ToolResult, tool_text,
19
- InputImage, InputAudio, InputVideo, InputFile, data_url)
21
+ InputImage, InputAudio, InputVideo, InputFile, data_url)
20
22
  from .streaming import *
21
23
  from .streaming import mk_acollect_stream
22
24
 
@@ -44,8 +46,8 @@ def norm_usage(resp):
44
46
  pt = int(usg.get("input_tokens", 0) or 0) + cached + cache_creation
45
47
  ct = int(usg.get("output_tokens", 0) or 0)
46
48
  return Usage(prompt_tokens=pt, completion_tokens=ct, total_tokens=pt + ct,
47
- cached_tokens=cached, cache_creation_tokens=cache_creation, reasoning_tokens=0, raw=usg,
48
- search_queries=int(nested_idx(usg, 'server_tool_use', 'web_search_requests') or 0))
49
+ cached_tokens=cached, cache_creation_tokens=cache_creation, reasoning_tokens=0, raw=usg,
50
+ search_queries=int(nested_idx(usg, 'server_tool_use', 'web_search_requests') or 0))
49
51
 
50
52
  def finalize_usage(usg, parts):
51
53
  "Split reasoning tokens out of `output_tokens`: the reported `thinking_tokens`, else an estimate from the thinking text."
@@ -110,7 +112,9 @@ def norm_tr_parts(content):
110
112
  if typ=='text': parts.append(Text(b.get('text',''), raw=b))
111
113
  elif typ=='image':
112
114
  s = b['source']
113
- parts.append(InputImage(f"data:{s['media_type']};base64,{s['data']}", mime=s['media_type'], raw=b))
115
+ if s['type']=='url': parts.append(InputImage(s['url'], raw=b))
116
+ elif s['type']=='base64': parts.append(InputImage(f"data:{s['media_type']};base64,{s['data']}", mime=s['media_type'], raw=b))
117
+ else: raise ValueError(f"Unsupported Anthropic image source type: {s['type']!r}")
114
118
  else: parts.append(mk_part(**b))
115
119
  return parts
116
120
 
@@ -238,13 +242,11 @@ def denorm_reasoning(v):
238
242
  # %% ../nbs/04_anthropic.ipynb #8fa9fbb8
239
243
  def denorm_web_search(v):
240
244
  "Map canonical web_search_options to Anthropic hosted web_search tool."
241
- _max_uses = {"low": 1, "medium": 5, "high": 10}
245
+ _max_uses = dict(low=1, medium=5, high=10)
242
246
  t = {"type": "web_search_20250305", "name": "web_search"}
243
247
  if (typ := (v or {}).get("type")): t["type"] = typ
244
- if (s := (v or {}).get("search_context_size")):
245
- t["max_uses"] = _max_uses.get(s, 5)
246
- if (u := (v or {}).get("user_location", {}).get("approximate")):
247
- t["user_location"] = {"type": "approximate", **u}
248
+ if (s := (v or {}).get("search_context_size")): t["max_uses"] = _max_uses.get(s, 5)
249
+ if (u := (v or {}).get("user_location", {}).get("approximate")): t["user_location"] = {"type": "approximate", **u}
248
250
  return t
249
251
 
250
252
  # %% ../nbs/04_anthropic.ipynb #6b485275
@@ -269,12 +271,12 @@ def denorm_user(m:Msg):
269
271
  def denorm_image(p, max_sz=None):
270
272
  if (b64:=data_url(p.text)):
271
273
  data = resize_b64(b64[1], max_sz) if max_sz else b64[1]
272
- return {"type": "image", "source": {"type": "base64", "media_type": b64[0], "data": data}}
274
+ return {"type": "image", "source": dict(type="base64", media_type=b64[0], data=data)}
273
275
  return {"type": "image", "source": {"type": "url", "url": p.text}}
274
276
 
275
277
  # %% ../nbs/04_anthropic.ipynb #fc6bbdfc
276
278
  def denorm_file(p):
277
- if (b64:=data_url(p.text)): return {"type": "document", "source": {"type": "base64", "media_type": b64[0], "data": b64[1]}}
279
+ if (b64:=data_url(p.text)): return {"type": "document", "source": dict(type="base64", media_type=b64[0], data=b64[1])}
278
280
  return {"type": "document", "source": {"type": "url", "url": p.text}}
279
281
 
280
282
  # %% ../nbs/04_anthropic.ipynb #37b63cc2
@@ -303,15 +305,21 @@ def _add_cache_control(
303
305
  return msg
304
306
 
305
307
  # %% ../nbs/04_anthropic.ipynb #6bc52ef5
308
+ def _clear_cache_control(obj):
309
+ "Clear canonical cache marks from a message or part."
310
+ if isinstance(obj, Msg):
311
+ for p in obj.content: _clear_cache_control(p)
312
+ elif isinstance(obj, Part): obj.cache_control = None
313
+
306
314
  def apply_cache_idxs(
307
315
  msgs, # Canonical Msgs to mark
308
316
  cache_idxs=[-1], # Breakpoint idxs into `msgs` (tool-role msgs excluded); 0 is `sp` when given
309
317
  sp=None, # System prompt: str, Msg, or Part
310
318
  ttl=None # Cache TTL: '5m' (default) or '1h'
311
319
  ):
312
- "Strip all cache breakpoints from `msgs`, then mark `cache_idxs`; returns `sp`, marked if 0 is included"
313
- for m in msgs:
314
- for p in m.content: p.cache_control = None
320
+ "Replace cache breakpoints in `msgs` and `sp`, then return the system prompt."
321
+ for m in msgs: _clear_cache_control(m)
322
+ _clear_cache_control(sp)
315
323
  if sp:
316
324
  if 0 in cache_idxs:
317
325
  if isinstance(sp, str): sp = Text(sp)
@@ -335,10 +343,8 @@ def mk_payload(msgs, model, **kwargs):
335
343
  if tools:=kwargs.get('tools'): payload['tools'] = denorm_tool_schs(tools)
336
344
  if tchc:=kwargs.get('tool_choice'): payload['tool_choice'] = denorm_tool_choice(tchc)
337
345
  if thk:=kwargs.get('reasoning_effort'): payload.update(denorm_reasoning(thk))
338
- if (wopts:=kwargs.get('web_search_options')) is not None:
339
- payload.setdefault('tools', []).append(denorm_web_search(wopts))
340
- if (temp:=kwargs.get('temperature')) is not None:
341
- payload['temperature'] = temp
346
+ if (wopts:=kwargs.get('web_search_options')) is not None: payload.setdefault('tools', []).append(denorm_web_search(wopts))
347
+ if (temp:=kwargs.get('temperature')) is not None: payload['temperature'] = temp
342
348
  return payload
343
349
 
344
350
  # %% ../nbs/04_anthropic.ipynb #60d52c1f
@@ -367,5 +373,4 @@ def cost(usage, m):
367
373
  # %% ../nbs/04_anthropic.ipynb #f7c0b989
368
374
  api_registry.register('anthropic', norm_tool_calls=norm_tool_calls, norm_parts=norm_parts, norm_finish=norm_finish, norm_usage=norm_usage,
369
375
  finalize_usage=finalize_usage, raw_msg=raw_msg, collate_raw=collate_raw, acollect_stream=acollect_stream, mk_payload=mk_payload,
370
- cost=cost, get_hdrs=get_hdrs,
371
- endpoint='/v1/messages')
376
+ cost=cost, get_hdrs=get_hdrs, endpoint='/v1/messages')
fastllm/chat.py CHANGED
@@ -1,4 +1,4 @@
1
- """High level chat api for fastllm similar to lisette"""
1
+ """A high-level chat API for fastllm, similar to Lisette"""
2
2
 
3
3
  # AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/07_chat.ipynb.
4
4
 
@@ -112,9 +112,12 @@ _cwe_msg = "ContextWindowExceededError: Do no more tool calls and complete your
112
112
 
113
113
  # %% ../nbs/07_chat.ipynb #05c20a94
114
114
  def search_count(r):
115
- if cnt := nested_idx(r.usage.raw, 'server_tool_use', 'web_search_requests'): return cnt # Anthropic
116
- if cnt := nested_idx(r.usage.raw, 'server_tool_use', 'google_search'): return cnt # Gemini
117
- if cnt := nested_idx(r.usage.raw, 'web_search_requests'): return cnt # streaming with `include_usage`
115
+ raw = r.usage.raw or {}
116
+ if (cnt := raw.get('web_search_requests')) is not None: return cnt # host aggregate
117
+ if cnt := r.usage.search_queries: return cnt
118
+ legacy = raw.get('server_tool_use') or {}
119
+ if (cnt := legacy.get('web_search_requests')) is not None: return cnt # Anthropic
120
+ if (cnt := legacy.get('google_search')) is not None: return cnt # Gemini
118
121
  return 0
119
122
 
120
123
  # %% ../nbs/07_chat.ipynb #61395e0d
@@ -288,8 +291,9 @@ async def _call(self:AsyncChat, msg=None, step=1, search=None, tool_choice=None,
288
291
  async for o in self._call_cbs('after_msgs'): yield o
289
292
 
290
293
  self.turn_kwargs, self.stream = kwargs, t.stream
291
- kw = merge(kwargs, dict(xtra_body=merge(kwargs.get('xtra_body'), initial_body))) if initial_body else kwargs
292
294
  async for o in self._call_cbs('before_acomplete'): yield o
295
+ kw = self.turn_kwargs
296
+ if initial_body: kw = merge(kw, dict(xtra_body=merge(kw.get('xtra_body'), initial_body)))
293
297
  res = await acomplete(self.turn_msgs, self.model, system=self.turn_sysp, stream=t.stream, tools=self.tool_schemas,
294
298
  tool_choice=tool_choice, max_tokens=int(max_tokens), temperature=None if t.think else ifnone(t.temp,self.temp),
295
299
  previous_response_id=self.response_id if self.use_previous_response_id else None,
@@ -413,7 +417,7 @@ class StopReasonCallback(ChatCallback):
413
417
  action,msg = _handle_stop_reason(self.turn_res)
414
418
  if action != 'warning': return
415
419
  add_warning(self.chat.turn_res, msg)
416
- if self.stream: yield dict(text=f"warning: {msg}\n\n")
420
+ if self.stream: yield Text(f'\n\nwarning: {msg}\n\n')
417
421
 
418
422
  # %% ../nbs/07_chat.ipynb #aa7630b2
419
423
  _lang2tool = dict(py='python', bash='bash')
@@ -493,11 +497,11 @@ defaults.chat_callbacks = [FenceToolCallback, ToolReminderCallback, StopReasonCa
493
497
 
494
498
  # %% ../nbs/07_chat.ipynb #ead4b56d
495
499
  class StreamAccum:
496
- "Accumulate a formatted stream: append each item's `formatted`, swap in `chat.full()` on `Refresh`"
500
+ "Accumulate formatted parts; refresh from `chat.full()` on `Refresh` or `Completion`"
497
501
  def __init__(self, chat=None, mx=2000): self.chat,self.mx,self.txt,self.last = chat,mx,'',None
498
502
  def __call__(self, o):
499
503
  "Feed one stream item; True if the display text changed"
500
- if isinstance(o, Refresh):
504
+ if isinstance(o, (Refresh, Completion)):
501
505
  if self.chat is None: return False
502
506
  self.txt,self.last = self.chat.full(mx=self.mx)+'\n\n',None
503
507
  elif f := getattr(o, 'formatted', ''):
@@ -509,7 +513,7 @@ class StreamAccum:
509
513
 
510
514
  # %% ../nbs/07_chat.ipynb #944bcd25
511
515
  async def adisplay_stream(rs, chat=None, mx=2000):
512
- "Use IPython.display to markdown display the response stream, refreshing from `chat.full()` on `Refresh`."
516
+ "Display a stream as Markdown, refreshing from `chat.full()` on `Refresh` or `Completion`."
513
517
  try: from IPython.display import display, Markdown
514
518
  except ModuleNotFoundError: raise ModuleNotFoundError("This function requires ipython. Please run `pip install ipython` to use.")
515
519
  acc,h = StreamAccum(chat, mx=mx), display(Markdown(' '), display_id=True)
fastllm/gemini.py CHANGED
@@ -1,3 +1,5 @@
1
+ """Translate Gemini generateContent requests, responses, and streams to and from fastllm's shared message types, including tools, thought signatures, search grounding, and media"""
2
+
1
3
  # AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/05_gemini.ipynb.
2
4
 
3
5
  # %% auto #0
@@ -16,7 +18,7 @@ from fasttransport.errors import api_error_from_event
16
18
 
17
19
  from .types import *
18
20
  from aidialog.msg_parts import (MdStr, Part, PartType, Msg, Completion, Text, Thinking, ToolUse, ToolResult, tool_text,
19
- InputImage, InputAudio, InputVideo, InputFile, sys_text, part_txt, data_url, url_mime)
21
+ InputImage, InputAudio, InputVideo, InputFile, sys_text, part_txt, data_url, url_mime)
20
22
  from .streaming import *
21
23
  from .streaming import mk_acollect_stream
22
24
 
@@ -41,9 +43,12 @@ def grounding(cand):
41
43
  return cites, call
42
44
 
43
45
  # %% ../nbs/05_gemini.ipynb #48de3125
44
- def _fn_calls(parts):
45
- return [ToolUse(id=fc.get("id", f"call_{i}"), name=fc.get("name", ""), arguments=fc.get("args") or {}, raw={k:v for k,v in p.items() if k != "functionCall"})
46
- for i,p in enumerate(parts) if (fc := p.get("functionCall"))]
46
+ def _fn_call(p, i):
47
+ fc = p['functionCall']
48
+ return ToolUse(id=fc.get("id", f"call_{i}"), name=fc.get("name", ""), arguments=fc.get("args") or {},
49
+ raw={k:v for k,v in p.items() if k != "functionCall"})
50
+
51
+ def _fn_calls(parts): return [_fn_call(p, i) for i,p in enumerate(parts) if p.get('functionCall')]
47
52
 
48
53
  def norm_tool_calls(resp):
49
54
  "Tool calls of a response: the `google_search` call its grounding describes, then its `functionCall` parts"
@@ -67,7 +72,7 @@ def norm_usage(resp):
67
72
  if call := grounding(cand)[1]: stu['google_search'] = len(call.arguments['queries'] or [1])
68
73
  if stu: usg['server_tool_use'] = stu
69
74
  return Usage(prompt_tokens=pt, completion_tokens=ct, total_tokens=tt,
70
- cached_tokens=cached, reasoning_tokens=reasoning, search_queries=stu.get('google_search', 0), raw=usg)
75
+ cached_tokens=cached, reasoning_tokens=reasoning, search_queries=stu.get('google_search', 0), raw=usg)
71
76
 
72
77
  # %% ../nbs/05_gemini.ipynb #7a8b1f8f
73
78
  def norm_finish(resp, tcs=None):
@@ -90,13 +95,11 @@ def norm_parts(resp):
90
95
  "Normalize Gemini generateContent response; a search's `toolCall` and `toolResponse` parts give way to the call `grounding` builds"
91
96
  c0 = nested_idx(resp, 'candidates', 0) or {}
92
97
  cites,call = grounding(c0)
93
- tc_map = {tc.id: tc for tc in norm_tool_calls(resp)}
94
98
  parts = [call] if call else []
95
- for p in nested_idx(c0, 'content', 'parts') or []:
99
+ for i,p in enumerate(nested_idx(c0, 'content', 'parts') or []):
96
100
  if 'toolCall' in p or 'toolResponse' in p: continue
97
101
  typ = _gem_part_type(p)
98
- if typ == PartType.tool_use:
99
- if tc := tc_map.get(p['functionCall'].get('id')): parts.append(tc)
102
+ if typ == PartType.tool_use: parts.append(_fn_call(p, i))
100
103
  elif typ == PartType.thinking: parts.append(Thinking(p.get("text",""), raw=p))
101
104
  elif typ == PartType.tool_result: parts.append(ToolResult(text=p.get("text",""), raw=p))
102
105
  else: parts.append(Text(p.get("text",""), raw=p))
@@ -175,15 +178,20 @@ def denorm_msgs(msgs:list[Msg]):
175
178
  return res
176
179
 
177
180
  # %% ../nbs/05_gemini.ipynb #adb525db
178
- _valid_gemini_sch = {'type', 'format', 'title', 'description', 'nullable', 'default',
179
- 'items', 'minItems', 'maxItems', 'enum', 'properties', 'propertyOrdering',
180
- 'required', 'minProperties', 'maxProperties', 'minimum', 'maximum',
181
- 'minLength', 'maxLength', 'pattern', 'example', 'anyOf'}
181
+ _valid_gemini_sch = {'type', 'format', 'title', 'description', 'nullable', 'default', 'items', 'minItems', 'maxItems', 'enum', 'properties',
182
+ 'propertyOrdering', 'required', 'minProperties', 'maxProperties', 'minimum', 'maximum', 'minLength', 'maxLength', 'pattern', 'example',
183
+ 'anyOf'}
182
184
 
183
185
  def _gem_filter_sch(s):
184
186
  if isinstance(s, list): return [_gem_filter_sch(x) for x in s]
185
187
  if not isinstance(s, dict): return s
186
- return {k: _gem_filter_sch(v) for k,v in s.items() if k in _valid_gemini_sch}
188
+ res = {}
189
+ for k,v in s.items():
190
+ if k not in _valid_gemini_sch: continue
191
+ if k == 'properties': res[k] = {name:_gem_filter_sch(prop) for name,prop in v.items()}
192
+ elif k in ('items', 'anyOf'): res[k] = _gem_filter_sch(v)
193
+ else: res[k] = v
194
+ return res
187
195
 
188
196
  def denorm_tool_schs(tools):
189
197
  "Convert canonical tools to Gemini format."
@@ -192,8 +200,7 @@ def denorm_tool_schs(tools):
192
200
  fn = fn_schema(t)
193
201
  if fn is None: other.append(t); continue
194
202
  name, desc, params = fn
195
- params = {k:v for k,v in params.items() if k in _valid_gemini_sch}
196
- params['properties'] = {k:_gem_filter_sch(v) for k,v in params['properties'].items()}
203
+ params = _gem_filter_sch(params)
197
204
  fn_decls.append(dict(name=name, description=desc, parameters=params))
198
205
  out = other[:]
199
206
  if fn_decls: out.insert(0, dict(functionDeclarations=fn_decls))
@@ -214,13 +221,11 @@ _gem_think_levels = dict(minimal='low', low='low', medium='medium', high='high'
214
221
 
215
222
  def denorm_reasoning(v, model=''):
216
223
  "Map canonical reasoning_effort to Gemini thinkingConfig (uses thinkingLevel for Gemini 3+)."
217
- err = ValueError(f"Invalid reasoning effort for Gemini: {v}, accepted string values are: {list(_gem_think_budgets)} and dicts are passthrough")
218
- if v is None: return None
219
- elif isinstance(v, dict): return v
220
- elif isinstance(v, str) and v in _gem_think_budgets:
221
- # defaults to includeThoughts same as litellm
222
- if 'gemini-3' in model: return {'thinkingLevel': _gem_think_levels.get(v, 'medium'), 'includeThoughts': True}
223
- return {'thinkingBudget': _gem_think_budgets.get(str(v).lower(), 1024), 'includeThoughts': True}
224
+ if v is None or isinstance(v, dict): return v
225
+ if not isinstance(v, str) or v not in _gem_think_budgets:
226
+ raise ValueError(f"Invalid reasoning effort for Gemini: {v!r}; expected one of {list(_gem_think_budgets)}, a dict, or None")
227
+ if 'gemini-3' in model: return {'thinkingLevel': _gem_think_levels[v], 'includeThoughts': True}
228
+ return {'thinkingBudget': _gem_think_budgets[v], 'includeThoughts': True}
224
229
 
225
230
  # %% ../nbs/05_gemini.ipynb #8fa9fbb8
226
231
  def denorm_web_search(v): return {"googleSearch": {}}
@@ -325,15 +330,7 @@ def cost(usage, m):
325
330
  return cost
326
331
 
327
332
  # %% ../nbs/05_gemini.ipynb #f7c0b989
328
- api_ns = dict(norm_tool_calls=norm_tool_calls,
329
- norm_parts=norm_parts,
330
- norm_finish=norm_finish,
331
- norm_usage=norm_usage,
332
- acollect_stream=acollect_stream,
333
- raw_msg=raw_msg,
334
- collate_raw=collate_raw,
335
- mk_payload=mk_payload,
336
- cost=cost,
337
- get_hdrs=get_hdrs,
338
- endpoint=endpoint)
333
+ api_ns = dict(norm_tool_calls=norm_tool_calls, norm_parts=norm_parts, norm_finish=norm_finish, norm_usage=norm_usage,
334
+ acollect_stream=acollect_stream, raw_msg=raw_msg, collate_raw=collate_raw, mk_payload=mk_payload, cost=cost, get_hdrs=get_hdrs,
335
+ endpoint=endpoint)
339
336
  api_registry.register('gemini', **api_ns)