python-fastllm 0.0.63__py3-none-any.whl → 0.0.65__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.63"
5
+ __version__ = "0.0.65"
fastllm/_modidx.py CHANGED
@@ -10,7 +10,6 @@ d = { 'settings': { 'branch': 'main',
10
10
  'fastllm.acomplete._classify_error': ('acomplete.html#_classify_error', 'fastllm/acomplete.py'),
11
11
  'fastllm.acomplete._classify_error_stream': ( 'acomplete.html#_classify_error_stream',
12
12
  'fastllm/acomplete.py'),
13
- 'fastllm.acomplete._codex_mtime_policy': ('acomplete.html#_codex_mtime_policy', 'fastllm/acomplete.py'),
14
13
  'fastllm.acomplete._debug_print': ('acomplete.html#_debug_print', 'fastllm/acomplete.py'),
15
14
  'fastllm.acomplete._is_ctx_exceeded': ('acomplete.html#_is_ctx_exceeded', 'fastllm/acomplete.py'),
16
15
  'fastllm.acomplete._raise_if_done': ('acomplete.html#_raise_if_done', 'fastllm/acomplete.py'),
@@ -112,7 +111,6 @@ d = { 'settings': { 'branch': 'main',
112
111
  'fastllm.chat._inject_tool_reminder': ('chat.html#_inject_tool_reminder', 'fastllm/chat.py'),
113
112
  'fastllm.chat._lite_call_func': ('chat.html#_lite_call_func', 'fastllm/chat.py'),
114
113
  'fastllm.chat._mk_tool_result': ('chat.html#_mk_tool_result', 'fastllm/chat.py'),
115
- 'fastllm.chat._think_kw': ('chat.html#_think_kw', 'fastllm/chat.py'),
116
114
  'fastllm.chat._usrtools': ('chat.html#_usrtools', 'fastllm/chat.py'),
117
115
  'fastllm.chat.add_warning': ('chat.html#add_warning', 'fastllm/chat.py'),
118
116
  'fastllm.chat.adisplay_stream': ('chat.html#adisplay_stream', 'fastllm/chat.py'),
@@ -126,7 +124,9 @@ d = { 'settings': { 'branch': 'main',
126
124
  'fastllm.chat.stop_reason': ('chat.html#stop_reason', 'fastllm/chat.py'),
127
125
  'fastllm.chat.structured': ('chat.html#structured', 'fastllm/chat.py'),
128
126
  'fastllm.chat.think_sep': ('chat.html#think_sep', 'fastllm/chat.py')},
129
- 'fastllm.codex': { 'fastllm.codex._merge_tokens': ('codex.html#_merge_tokens', 'fastllm/codex.py'),
127
+ 'fastllm.codex': { 'fastllm.codex._codex_hdrs': ('codex.html#_codex_hdrs', 'fastllm/codex.py'),
128
+ 'fastllm.codex._codex_vendor_auth': ('codex.html#_codex_vendor_auth', 'fastllm/codex.py'),
129
+ 'fastllm.codex._merge_tokens': ('codex.html#_merge_tokens', 'fastllm/codex.py'),
130
130
  'fastllm.codex._post': ('codex.html#_post', 'fastllm/codex.py'),
131
131
  'fastllm.codex._token_grant': ('codex.html#_token_grant', 'fastllm/codex.py'),
132
132
  'fastllm.codex.codex_auth': ('codex.html#codex_auth', 'fastllm/codex.py'),
@@ -373,10 +373,11 @@ d = { 'settings': { 'branch': 'main',
373
373
  'fastllm.types.Usage.__add__': ('types.html#usage.__add__', 'fastllm/types.py'),
374
374
  'fastllm.types.Usage.__eq__': ('types.html#usage.__eq__', 'fastllm/types.py'),
375
375
  'fastllm.types.Usage.__init__': ('types.html#usage.__init__', 'fastllm/types.py'),
376
- 'fastllm.types._flat_rates': ('types.html#_flat_rates', 'fastllm/types.py'),
377
376
  'fastllm.types.approx_pricing': ('types.html#approx_pricing', 'fastllm/types.py'),
378
377
  'fastllm.types.effort_code': ('types.html#effort_code', 'fastllm/types.py'),
378
+ 'fastllm.types.effort_kwargs': ('types.html#effort_kwargs', 'fastllm/types.py'),
379
379
  'fastllm.types.effort_levels': ('types.html#effort_levels', 'fastllm/types.py'),
380
+ 'fastllm.types.find_models': ('types.html#find_models', 'fastllm/types.py'),
380
381
  'fastllm.types.fn_schema': ('types.html#fn_schema', 'fastllm/types.py'),
381
382
  'fastllm.types.get_api_key': ('types.html#get_api_key', 'fastllm/types.py'),
382
383
  'fastllm.types.get_model_info': ('types.html#get_model_info', 'fastllm/types.py'),
@@ -390,6 +391,7 @@ d = { 'settings': { 'branch': 'main',
390
391
  'fastllm.types.price_tier': ('types.html#price_tier', 'fastllm/types.py'),
391
392
  'fastllm.types.provider_req': ('types.html#provider_req', 'fastllm/types.py'),
392
393
  'fastllm.types.register_model_info': ('types.html#register_model_info', 'fastllm/types.py'),
394
+ 'fastllm.types.register_sub_model': ('types.html#register_sub_model', 'fastllm/types.py'),
393
395
  'fastllm.types.resize_b64': ('types.html#resize_b64', 'fastllm/types.py'),
394
396
  'fastllm.types.resolve_effort': ('types.html#resolve_effort', 'fastllm/types.py'),
395
397
  'fastllm.types.tier_rate': ('types.html#tier_rate', 'fastllm/types.py'),
fastllm/acomplete.py CHANGED
@@ -1,11 +1,11 @@
1
1
  """A unified async API for model completions and streaming"""
2
2
 
3
- # AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/06_acomplete.ipynb.
3
+ # AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/03_acomplete.ipynb.
4
4
 
5
5
  # %% auto #0
6
6
  __all__ = ['vendor_mapping', 'defaults', 'split_vendor', 'mk_client', 'ContextWindowExceededError', 'acomplete']
7
7
 
8
- # %% ../nbs/06_acomplete.ipynb #f2f57253
8
+ # %% ../nbs/03_acomplete.ipynb #f2f57253
9
9
  import asyncio,json,httpx2
10
10
  from fastcore.utils import *
11
11
  from fastcore.meta import *
@@ -13,7 +13,7 @@ from fasttransport.core import AsyncHttpCli
13
13
  from fasttransport.errors import APIError
14
14
 
15
15
  from .types import *
16
- from .codex import codex_auth_path
16
+ from .codex import *
17
17
  from aidialog.msg_parts import (Part, PartType, Msg, Text, Thinking, ToolUse, ToolResult, InputImage, InputAudio, InputVideo,
18
18
  InputFile, mk_tool_res_msg, Completion)
19
19
  from .streaming import *
@@ -22,15 +22,13 @@ from .openai_chat import *
22
22
  from .anthropic import *
23
23
  from .gemini import *
24
24
 
25
- # %% ../nbs/06_acomplete.ipynb #32ee2546
26
- _codex_path = codex_auth_path()
27
- _codex_json = _codex_path, ('tokens','access_token')
25
+ # %% ../nbs/03_acomplete.ipynb #32ee2546
28
26
  vendor_mapping = {
29
27
  "openai": ('openai', 'https://api.openai.com/v1', 'OPENAI_API_KEY'),
30
28
  "anthropic": ('anthropic', 'https://api.anthropic.com', 'ANTHROPIC_API_KEY'),
31
29
  "gemini": ('gemini', 'https://generativelanguage.googleapis.com/', 'GEMINI_API_KEY'),
32
30
  "openai_chat": ('openai_chat', 'https://api.openai.com/v1', 'OPENAI_API_KEY'),
33
- "codex": ('openai', 'https://chatgpt.com/backend-api/codex', 'CODEX_AUTH_TOKEN', _codex_json),
31
+ "codex": ('openai', 'https://chatgpt.com/backend-api/codex', 'CODEX_AUTH_TOKEN'),
34
32
  "moonshot": ('openai_chat', "https://api.moonshot.ai/v1", "MOONSHOT_API_KEY"),
35
33
  "deepseek": ('openai_chat', "https://api.deepseek.com/v1", "DEEPSEEK_API_KEY"),
36
34
  "inception": ('openai_chat', "https://api.inceptionlabs.ai/v1", "INCEPTION_API_KEY"),
@@ -43,7 +41,7 @@ vendor_mapping = {
43
41
  "meta": ('openai_chat', "https://api.meta.ai/v1", "META_API_KEY"),
44
42
  "zai": ('openai_chat', "https://api.z.ai/api/paas/v4", "ZAI_API_KEY")}
45
43
 
46
- # %% ../nbs/06_acomplete.ipynb #e3ed40fb
44
+ # %% ../nbs/03_acomplete.ipynb #e3ed40fb
47
45
  def split_vendor(model):
48
46
  "Split a `'vendor/model'` path into `(vendor_name, model)`: the prefix must name a known vendor or a registered api (installed `fastllm.apis` plugins load on demand), else `(None, model)`."
49
47
  if model and '/' in model:
@@ -51,23 +49,8 @@ def split_vendor(model):
51
49
  if v in vendor_mapping or v in api_registry: return v, m
52
50
  return None, model
53
51
 
54
- # %% ../nbs/06_acomplete.ipynb #977a115b
55
- def _codex_mtime_policy():
56
- "`flexicache` policy: expire codex clients when the codex auth file changes"
57
- def policy(state, args, kwargs):
58
- v = kwargs.get('vendor_name') or (args[1] if len(args)>1 else None)
59
- if not v:
60
- m = kwargs.get('model') or (args[0] if args else None)
61
- v = split_vendor(m)[0] if m else None
62
- if v != 'codex': return None
63
- try: mt = Path(_codex_path).expanduser().stat().st_mtime
64
- except OSError: mt = None
65
- return mt if state != mt else None
66
- policy.needs_args = True
67
- return policy
68
-
69
- # %% ../nbs/06_acomplete.ipynb #79075d95
70
- @flexicache(_codex_mtime_policy())
52
+ # %% ../nbs/03_acomplete.ipynb #79075d95
53
+ @flexicache()
71
54
  def mk_client(model=None, vendor_name=None, api_name=None, api_key=None, oauth_token=None, base_url=None, xtra_hdrs=None,
72
55
  timeout=httpx2.Timeout(connect=30, read=300, write=30, pool=10)):
73
56
  err_msg = f"please pass a valid one vendor: {', '.join(list(vendor_mapping))} or pass `api_name`,`base_url` and `api_key`"
@@ -77,12 +60,8 @@ def mk_client(model=None, vendor_name=None, api_name=None, api_key=None, oauth_t
77
60
  if vendor_name:
78
61
  override_base_url = base_url
79
62
  try:
80
- api_name, base_url, env_api_nm, *auth_json = vendor_mapping[vendor_name]
63
+ api_name, base_url, env_api_nm = vendor_mapping[vendor_name]
81
64
  base_url = override_base_url or base_url
82
- if auth_json and not api_key and not oauth_token and not os.getenv(env_api_nm):
83
- fn,keys = auth_json[0] # pyright: ignore[reportAssignmentType]
84
- auth_fn = Path(fn).expanduser()
85
- if auth_fn.exists(): api_key = nested_idx(json.loads(auth_fn.read_text()), *keys)
86
65
  if not oauth_token: api_key = get_api_key(api_key, env_api_nm)
87
66
  except KeyError: raise ValueError(f"Unknown vendor '{vendor_name}', {err_msg}")
88
67
  elif base_url and (api_key or oauth_token): vendor_name, api_name = ifnone(vendor_name, 'custom'), ifnone(api_name, 'openai_chat')
@@ -93,7 +72,7 @@ def mk_client(model=None, vendor_name=None, api_name=None, api_key=None, oauth_t
93
72
  cli = AsyncHttpCli(base_url, base_headers=hdrs, timeout=timeout)
94
73
  return cli, api_name, vendor_name
95
74
 
96
- # %% ../nbs/06_acomplete.ipynb #df851a5c
75
+ # %% ../nbs/03_acomplete.ipynb #df851a5c
97
76
  class ContextWindowExceededError(APIError): pass
98
77
 
99
78
  def _is_ctx_exceeded(code, msg):
@@ -119,8 +98,8 @@ async def _classify_error_stream(gen):
119
98
  async for x in gen: yield x
120
99
  except APIError as e: raise _classify_error(e) from e
121
100
 
122
- # %% ../nbs/06_acomplete.ipynb #f626a4e1
123
- defaults = SimpleNamespace(debug_mode=False)
101
+ # %% ../nbs/03_acomplete.ipynb #f626a4e1
102
+ defaults = SimpleNamespace(debug_mode=False, reasoning_effort=None)
124
103
 
125
104
  def _debug_print(model, api_name, vendor_name, payload, url):
126
105
  "Pretty-print acomplete inputs when defaults.debug_mode is set"
@@ -133,7 +112,7 @@ def _debug_print(model, api_name, vendor_name, payload, url):
133
112
  print(f"\033[1;33mpayload:\033[0m\n{pformat(p, width=120, sort_dicts=False)}")
134
113
  print('━'*60)
135
114
 
136
- # %% ../nbs/06_acomplete.ipynb #497c8565
115
+ # %% ../nbs/03_acomplete.ipynb #497c8565
137
116
  async def _raise_if_done(e, n, retries, retry_delay, yielded=False):
138
117
  e = _classify_error(e)
139
118
  if yielded or not e.retryable or n == retries: raise e
@@ -154,7 +133,7 @@ async def _retry_stream(mk_gen, retries=2, retry_delay=0.5):
154
133
  return
155
134
  except APIError as e: await _raise_if_done(e, n, retries, retry_delay, yielded=yielded)
156
135
 
157
- # %% ../nbs/06_acomplete.ipynb #2379ec94
136
+ # %% ../nbs/03_acomplete.ipynb #2379ec94
158
137
  @delegates(payload_kwargs)
159
138
  async def acomplete(msgs, model, api_name=None, vendor_name=None, api_key=None, oauth_token=None, base_url=None, endpoint=None, xtra_body=None, xtra_hdrs=None,
160
139
  stream=False, previous_response_id=None, stop_callables=None, retries=2, retry_delay=0.5, **kwargs):
@@ -168,7 +147,12 @@ async def acomplete(msgs, model, api_name=None, vendor_name=None, api_key=None,
168
147
  if own:
169
148
  vendor_name = ifnone(vendor_name, api_name) # an own-transport api: no HTTP client to build
170
149
  if oauth_token: kwargs['oauth_token'] = oauth_token # its `mk_payload` applies the user's token itself
171
- else: cli, api_name, vendor_name = mk_client(model=model, vendor_name=vendor_name, api_name=api_name, api_key=api_key, oauth_token=oauth_token, base_url=base_url)
150
+ else:
151
+ auth = vendor_auth.get(vendor_name)
152
+ if auth and not (api_key or oauth_token or os.getenv(vendor_mapping[vendor_name][2])): api_key = await auth()
153
+ cli, api_name, vendor_name = mk_client(model=model, vendor_name=vendor_name, api_name=api_name, api_key=api_key, oauth_token=oauth_token, base_url=base_url)
154
+ if (eff := defaults.reasoning_effort) and 'reasoning_effort' not in kwargs and effort_levels(get_model_info(model, vendor_name)):
155
+ kwargs |= effort_kwargs(model, eff, vendor_name)
172
156
  api = api_registry[api_name]
173
157
  if previous_response_id is not None:
174
158
  if not getattr(api, 'supports_previous_response_id', False):
@@ -179,8 +163,7 @@ async def acomplete(msgs, model, api_name=None, vendor_name=None, api_key=None,
179
163
  body = payload | ifnone(xtra_body, {})
180
164
  if not own:
181
165
  headers = xtra_hdrs or {}
182
- if vendor_name == 'codex' and (key := body.get('prompt_cache_key')):
183
- if not any(k.lower() == 'session-id' for k in headers): headers = {**headers, 'session-id':key}
166
+ if hk := vendor_hdrs.get(vendor_name): headers = hk(body, headers)
184
167
  ep = endpoint or api.endpoint
185
168
  path,params = ep(model, stream) if callable(ep) else (ep, None)
186
169
  if defaults.debug_mode: _debug_print(model, api_name, vendor_name, body, None if own else cli._url(path))
fastllm/anthropic.py CHANGED
@@ -1,6 +1,6 @@
1
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
2
 
3
- # AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/04_anthropic.ipynb.
3
+ # AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/06_anthropic.ipynb.
4
4
 
5
5
  # %% auto #0
6
6
  __all__ = ['ant_tc_types', 'norm_tool_call', 'norm_tool_calls', 'norm_usage', 'finalize_usage', 'norm_finish', 'search_results',
@@ -9,9 +9,8 @@ __all__ = ['ant_tc_types', 'norm_tool_call', 'norm_tool_calls', 'norm_usage', 'f
9
9
  'denorm_tool_choice', 'denorm_reasoning', 'denorm_web_search', 'denorm_system', 'denorm_user',
10
10
  'denorm_image', 'denorm_file', 'denorm_tool_result', 'apply_cache_idxs', 'mk_payload', 'get_hdrs', 'cost']
11
11
 
12
- # %% ../nbs/04_anthropic.ipynb #02afd3d7
12
+ # %% ../nbs/06_anthropic.ipynb #02afd3d7
13
13
  import json, copy
14
- from collections import Counter
15
14
  from fastcore.utils import *
16
15
  from fastcore.meta import *
17
16
  from fasttransport.errors import api_error_from_event
@@ -22,7 +21,7 @@ from aidialog.msg_parts import (MdStr, Part, PartType, mk_part, Msg, Completion,
22
21
  from .streaming import *
23
22
  from .streaming import mk_acollect_stream
24
23
 
25
- # %% ../nbs/04_anthropic.ipynb #5656d1d6
24
+ # %% ../nbs/06_anthropic.ipynb #5656d1d6
26
25
  ant_tc_types = ("tool_use", "server_tool_use", "mcp_tool_use")
27
26
 
28
27
  def norm_tool_call(b):
@@ -37,7 +36,7 @@ def norm_tool_calls(resp, delta=False):
37
36
  if tc:= norm_tool_call(b): out.append(tc)
38
37
  return out
39
38
 
40
- # %% ../nbs/04_anthropic.ipynb #0a1e2a8e
39
+ # %% ../nbs/06_anthropic.ipynb #0a1e2a8e
41
40
  def norm_usage(resp):
42
41
  "Normalize Anthropic usage shape."
43
42
  if not (usg:=resp.get("usage")): return None
@@ -58,7 +57,7 @@ def finalize_usage(usg, parts):
58
57
  return Usage(prompt_tokens=usg.prompt_tokens, completion_tokens=ct-rt, total_tokens=usg.prompt_tokens+ct,
59
58
  cached_tokens=usg.cached_tokens, cache_creation_tokens=usg.cache_creation_tokens, reasoning_tokens=rt, search_queries=usg.search_queries, raw=usg.raw)
60
59
 
61
- # %% ../nbs/04_anthropic.ipynb #7a8b1f8f
60
+ # %% ../nbs/06_anthropic.ipynb #7a8b1f8f
62
61
  def norm_finish(resp, tcs=None):
63
62
  "Canonicalize finish_reason to OpenAI Chat values: stop, tool_calls, length, content_filter."
64
63
  reason = resp.get("stop_reason")
@@ -66,7 +65,7 @@ def norm_finish(resp, tcs=None):
66
65
  r = mp.get(reason, reason)
67
66
  return FinishReason.tool_calls if r==FinishReason.stop and any(~L(tcs).attrgot('server')) else r
68
67
 
69
- # %% ../nbs/04_anthropic.ipynb #9796be28
68
+ # %% ../nbs/06_anthropic.ipynb #9796be28
70
69
  def search_results(parts, blocks):
71
70
  "Give each server call the sources its `web_search_tool_result` block reports, one `title: url` line each, or the block's error code"
72
71
  calls = {p.id: p for p in parts if isinstance(p, ToolUse) and p.server}
@@ -76,7 +75,7 @@ def search_results(parts, blocks):
76
75
  call.text = res.get('error_code', '') if isinstance(res, dict) else MdStr('\n'.join(f"{r.get('title', '')}: {r['url']}" for r in res if r.get('url')))
77
76
  return parts
78
77
 
79
- # %% ../nbs/04_anthropic.ipynb #20d2e803
78
+ # %% ../nbs/06_anthropic.ipynb #20d2e803
80
79
  def _ant_part_type(typ):
81
80
  "Map Anthropic content block type to canonical PartType."
82
81
  ant_parts_mapping = dict(text=PartType.text, thinking=PartType.thinking, redacted_thinking=PartType.thinking, tool_use=PartType.tool_use, server_tool_use=PartType.tool_use, mcp_tool_use=PartType.tool_use)
@@ -97,12 +96,12 @@ def norm_parts(resp):
97
96
  parts.append(Text(b.get("text", ""), citations=cites, raw=b))
98
97
  return search_results(parts, resp.get("content", []))
99
98
 
100
- # %% ../nbs/04_anthropic.ipynb #f24e52db
99
+ # %% ../nbs/06_anthropic.ipynb #f24e52db
101
100
  def raw_msg(resp):
102
101
  "The assistant message as the API sent it"
103
102
  return dict(role='assistant', content=resp['content'])
104
103
 
105
- # %% ../nbs/04_anthropic.ipynb #5d1440b7
104
+ # %% ../nbs/06_anthropic.ipynb #5d1440b7
106
105
  def norm_tr_parts(content):
107
106
  "Normalize an Anthropic `tool_result` content str or block list to canonical `Part`s"
108
107
  if isinstance(content, str): return [Text(content)]
@@ -118,7 +117,7 @@ def norm_tr_parts(content):
118
117
  else: parts.append(mk_part(**b))
119
118
  return parts
120
119
 
121
- # %% ../nbs/04_anthropic.ipynb #a3869e31
120
+ # %% ../nbs/06_anthropic.ipynb #a3869e31
122
121
  def norm_sse_event(ev, **kwargs):
123
122
  ev = obj2dict(ev)
124
123
  typ = ev.get("type")
@@ -142,12 +141,12 @@ def norm_sse_event(ev, **kwargs):
142
141
  elif typ == "error": raise api_error_from_event(ev)
143
142
  return Delta(text=text, thinking=thinking, tool_calls=tcs, citations=citations, raw=ev, **kwargs)
144
143
 
145
- # %% ../nbs/04_anthropic.ipynb #33cd3bd3
144
+ # %% ../nbs/06_anthropic.ipynb #33cd3bd3
146
145
  def delta_index_fn(d, typ, last_typ, last_idx):
147
146
  'Returns accumulation index for current delta and updated last idx'
148
147
  return nested_idx(d, 'raw', 'index'), None
149
148
 
150
- # %% ../nbs/04_anthropic.ipynb #de900e34
149
+ # %% ../nbs/06_anthropic.ipynb #de900e34
151
150
  def collate_raw(deltas):
152
151
  "The assistant message rebuilt from a stream's events"
153
152
  blocks = {}
@@ -164,7 +163,7 @@ def collate_raw(deltas):
164
163
  elif typ == 'content_block_stop' and '_json' in blocks[i]: blocks[i]['input'] = json.loads(blocks[i].pop('_json') or '{}')
165
164
  return dict(role='assistant', content=list(blocks.values()))
166
165
 
167
- # %% ../nbs/04_anthropic.ipynb #c994adb1
166
+ # %% ../nbs/06_anthropic.ipynb #c994adb1
168
167
  @delegates(mk_acollect_stream, but=['index_fn', 'api_name'])
169
168
  async def acollect_stream(resp, **kwargs):
170
169
  res = mk_acollect_stream(norm_and_yield(resp, norm_sse_event), index_fn=delta_index_fn, api_name='anthropic', **kwargs)
@@ -172,16 +171,16 @@ async def acollect_stream(resp, **kwargs):
172
171
  if isinstance(o, Completion): search_results(o.message.content, o.message.raw['content'])
173
172
  yield o
174
173
 
175
- # %% ../nbs/04_anthropic.ipynb #bb11ab62
174
+ # %% ../nbs/06_anthropic.ipynb #bb11ab62
176
175
  def _ant_cc(block, p):
177
176
  "Add the part's prompt-cache directive to `block`, if it has one."
178
177
  if p.cache_control: block['cache_control'] = p.cache_control
179
178
  return block
180
179
 
181
- # %% ../nbs/04_anthropic.ipynb #62e9a042
180
+ # %% ../nbs/06_anthropic.ipynb #62e9a042
182
181
  def _sanid(id_str): return re.sub(r'[^a-zA-Z0-9_-]', '_', id_str or '')
183
182
 
184
- # %% ../nbs/04_anthropic.ipynb #6ec772cb
183
+ # %% ../nbs/06_anthropic.ipynb #6ec772cb
185
184
  def denorm_tool_use(p:ToolUse):
186
185
  "Convert canonical tool_use Part to Anthropic tool_use content block."
187
186
  return _ant_cc(dict(type='tool_use', id=_sanid(p.id or ''), name=p.name or '', input=p.arguments or {}), p)
@@ -209,7 +208,7 @@ def denorm_msgs(msgs:list[Msg]):
209
208
  elif m.role == 'tool': res.append(denorm_tool(m))
210
209
  return res
211
210
 
212
- # %% ../nbs/04_anthropic.ipynb #f762a6f2
211
+ # %% ../nbs/06_anthropic.ipynb #f762a6f2
213
212
  def denorm_tool_schs(tools):
214
213
  "Convert canonical tools to Anthropic format."
215
214
  out = []
@@ -220,7 +219,7 @@ def denorm_tool_schs(tools):
220
219
  out.append(dict(name=name, description=desc, input_schema=params))
221
220
  return out
222
221
 
223
- # %% ../nbs/04_anthropic.ipynb #6118d7ea
222
+ # %% ../nbs/06_anthropic.ipynb #6118d7ea
224
223
  def denorm_tool_choice(v):
225
224
  "Map canonical tool_choice to Anthropic format."
226
225
  if v is None: return None
@@ -229,7 +228,7 @@ def denorm_tool_choice(v):
229
228
  if v in ('none', 'off', 'disabled'): return {'type': 'none'}
230
229
  return {'type': 'tool', 'name': v}
231
230
 
232
- # %% ../nbs/04_anthropic.ipynb #310f1e75
231
+ # %% ../nbs/06_anthropic.ipynb #310f1e75
233
232
  def denorm_reasoning(v):
234
233
  "Map canonical reasoning_effort to Anthropic adaptive thinking + output_config."
235
234
  mp = dict(minimal='low', low='low', medium='medium', high='high', xhigh='xhigh', max='max')
@@ -239,7 +238,7 @@ def denorm_reasoning(v):
239
238
  elif isinstance(v, str) and v in mp: return {"thinking": {"type": "adaptive"}, "output_config": {"effort": mp.get(v)}}
240
239
  raise err
241
240
 
242
- # %% ../nbs/04_anthropic.ipynb #8fa9fbb8
241
+ # %% ../nbs/06_anthropic.ipynb #8fa9fbb8
243
242
  def denorm_web_search(v):
244
243
  "Map canonical web_search_options to Anthropic hosted web_search tool."
245
244
  _max_uses = dict(low=1, medium=5, high=10)
@@ -249,13 +248,13 @@ def denorm_web_search(v):
249
248
  if (u := (v or {}).get("user_location", {}).get("approximate")): t["user_location"] = {"type": "approximate", **u}
250
249
  return t
251
250
 
252
- # %% ../nbs/04_anthropic.ipynb #6b485275
251
+ # %% ../nbs/06_anthropic.ipynb #6b485275
253
252
  def denorm_system(sp):
254
253
  if isinstance(sp, Msg): sp = sp.content[0]
255
254
  if isinstance(sp, Part): return [_ant_cc(dict(type='text', text=sp.text), sp)]
256
255
  else: return sp
257
256
 
258
- # %% ../nbs/04_anthropic.ipynb #1b990cc9
257
+ # %% ../nbs/06_anthropic.ipynb #1b990cc9
259
258
  def denorm_user(m:Msg):
260
259
  "Convert canonical user Msg to Anthropic user message."
261
260
  parts = []
@@ -267,19 +266,19 @@ def denorm_user(m:Msg):
267
266
  elif isinstance(p, InputFile): parts.append(_ant_cc(denorm_file(p), p))
268
267
  return dict(role='user', content=parts)
269
268
 
270
- # %% ../nbs/04_anthropic.ipynb #edd87272
269
+ # %% ../nbs/06_anthropic.ipynb #edd87272
271
270
  def denorm_image(p, max_sz=None):
272
271
  if (b64:=data_url(p.text)):
273
272
  data = resize_b64(b64[1], max_sz) if max_sz else b64[1]
274
273
  return {"type": "image", "source": dict(type="base64", media_type=b64[0], data=data)}
275
274
  return {"type": "image", "source": {"type": "url", "url": p.text}}
276
275
 
277
- # %% ../nbs/04_anthropic.ipynb #fc6bbdfc
276
+ # %% ../nbs/06_anthropic.ipynb #fc6bbdfc
278
277
  def denorm_file(p):
279
278
  if (b64:=data_url(p.text)): return {"type": "document", "source": dict(type="base64", media_type=b64[0], data=b64[1])}
280
279
  return {"type": "document", "source": {"type": "url", "url": p.text}}
281
280
 
282
- # %% ../nbs/04_anthropic.ipynb #37b63cc2
281
+ # %% ../nbs/06_anthropic.ipynb #37b63cc2
283
282
  def denorm_tool_result(p:ToolResult):
284
283
  "Convert canonical tool_result Part to Anthropic tool_result content block."
285
284
  tid = _sanid(p.id or (p.raw or {}).get('call_id',''))
@@ -293,7 +292,7 @@ def denorm_tool_result(p:ToolResult):
293
292
  return _ant_cc(dict(type='tool_result', tool_use_id=tid, content=blocks), p)
294
293
  return _ant_cc(dict(type='tool_result', tool_use_id=tid, content=tool_text(p.text)), p)
295
294
 
296
- # %% ../nbs/04_anthropic.ipynb #e4eb8d3b
295
+ # %% ../nbs/06_anthropic.ipynb #e4eb8d3b
297
296
  def _add_cache_control(
298
297
  msg, # Canonical Msg to mark
299
298
  ttl=None # Cache TTL: '5m' (default) or '1h'
@@ -304,7 +303,7 @@ def _add_cache_control(
304
303
  if idxs: msg.content[idxs[-1]].cache_control = cc
305
304
  return msg
306
305
 
307
- # %% ../nbs/04_anthropic.ipynb #6bc52ef5
306
+ # %% ../nbs/06_anthropic.ipynb #6bc52ef5
308
307
  def _clear_cache_control(obj):
309
308
  "Clear canonical cache marks from a message or part."
310
309
  if isinstance(obj, Msg):
@@ -332,12 +331,14 @@ def apply_cache_idxs(
332
331
  except IndexError: continue
333
332
  return sp
334
333
 
335
- # %% ../nbs/04_anthropic.ipynb #587d4fe5
334
+ # %% ../nbs/06_anthropic.ipynb #587d4fe5
336
335
  @delegates(payload_kwargs)
337
336
  def mk_payload(msgs, model, **kwargs):
338
337
  sp = kwargs.get('system')
339
338
  if (ci:=kwargs.get('cache_idxs')) is not None: sp = apply_cache_idxs(msgs, ci, sp, kwargs.get('ttl'))
340
- payload = dict(model=model, messages=denorm_msgs(msgs), max_tokens=kwargs.get('max_tokens') or 1024)
339
+ max_tokens = kwargs.get('max_tokens')
340
+ if max_tokens is None: max_tokens = get_model_info(model, 'anthropic').get('max_output_tokens') or 1024
341
+ payload = dict(model=model, messages=denorm_msgs(msgs), max_tokens=max_tokens)
341
342
  if kwargs.get('stream'): payload['stream'] = True
342
343
  if sp: payload['system'] = denorm_system(sp)
343
344
  if tools:=kwargs.get('tools'): payload['tools'] = denorm_tool_schs(tools)
@@ -347,7 +348,7 @@ def mk_payload(msgs, model, **kwargs):
347
348
  if (temp:=kwargs.get('temperature')) is not None: payload['temperature'] = temp
348
349
  return payload
349
350
 
350
- # %% ../nbs/04_anthropic.ipynb #60d52c1f
351
+ # %% ../nbs/06_anthropic.ipynb #60d52c1f
351
352
  def get_hdrs(api_key=None, oauth_token=None):
352
353
  "An OAuth token travels as a bearer under Anthropic's OAuth beta; an API key as `x-api-key`"
353
354
  hdrs = {"anthropic-version": "2023-06-01"}
@@ -355,22 +356,18 @@ def get_hdrs(api_key=None, oauth_token=None):
355
356
  return hdrs | {"x-api-key": get_api_key(api_key, 'ANTHROPIC_API_KEY')}
356
357
 
357
358
 
358
- # %% ../nbs/04_anthropic.ipynb #0d03643a
359
+ # %% ../nbs/06_anthropic.ipynb #0d03643a
359
360
  def cost(usage, m):
360
361
  raw = usage.raw
361
- in_tok = raw['input_tokens']
362
- cache_read = raw.get('cache_read_input_tokens', 0)
363
362
  cc = raw.get('cache_creation', {}) or {}
364
- cache_5m = cc.get('ephemeral_5m_input_tokens', raw.get('cache_creation_input_tokens', 0))
365
- cache_1h = cc.get('ephemeral_1h_input_tokens', 0)
366
- cost = in_tok * m.input_cost_per_token
367
- cost += raw['output_tokens'] * m.output_cost_per_token
368
- cost += cache_read * m.get('cache_read_input_token_cost', 0)
369
- cost += cache_5m * m.get('cache_creation_input_token_cost', 0)
370
- cost += cache_1h * m.get('cache_creation_input_token_cost_above_1hr', 0)
371
- return cost
372
-
373
- # %% ../nbs/04_anthropic.ipynb #f7c0b989
363
+ prompt = dict(input_cost_per_token=raw['input_tokens'], cache_read_input_token_cost=raw.get('cache_read_input_tokens', 0),
364
+ cache_creation_input_token_cost=cc.get('ephemeral_5m_input_tokens', raw.get('cache_creation_input_tokens', 0)),
365
+ cache_creation_input_token_cost_above_1hr=cc.get('ephemeral_1h_input_tokens', 0))
366
+ tier = price_tier(m, sum(prompt.values()))
367
+ toks = prompt | dict(output_cost_per_token=raw['output_tokens'])
368
+ return sum(n * tier_rate(m, k, tier) for k,n in toks.items())
369
+
370
+ # %% ../nbs/06_anthropic.ipynb #f7c0b989
374
371
  api_registry.register('anthropic', norm_tool_calls=norm_tool_calls, norm_parts=norm_parts, norm_finish=norm_finish, norm_usage=norm_usage,
375
372
  finalize_usage=finalize_usage, raw_msg=raw_msg, collate_raw=collate_raw, acollect_stream=acollect_stream, mk_payload=mk_payload,
376
373
  cost=cost, get_hdrs=get_hdrs, endpoint='/v1/messages')