python-fastllm 0.0.63__tar.gz → 0.0.64__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 (23) hide show
  1. {python_fastllm-0.0.63 → python_fastllm-0.0.64}/PKG-INFO +12 -12
  2. {python_fastllm-0.0.63 → python_fastllm-0.0.64}/README.md +11 -11
  3. {python_fastllm-0.0.63 → python_fastllm-0.0.64}/fastllm/__init__.py +1 -1
  4. {python_fastllm-0.0.63 → python_fastllm-0.0.64}/fastllm/_modidx.py +6 -4
  5. {python_fastllm-0.0.63 → python_fastllm-0.0.64}/fastllm/acomplete.py +21 -38
  6. {python_fastllm-0.0.63 → python_fastllm-0.0.64}/fastllm/anthropic.py +31 -31
  7. {python_fastllm-0.0.63 → python_fastllm-0.0.64}/fastllm/chat.py +50 -55
  8. {python_fastllm-0.0.63 → python_fastllm-0.0.64}/fastllm/codex.py +13 -0
  9. {python_fastllm-0.0.63 → python_fastllm-0.0.64}/fastllm/gemini.py +29 -29
  10. {python_fastllm-0.0.63 → python_fastllm-0.0.64}/fastllm/model_prices.json +35543 -21660
  11. {python_fastllm-0.0.63 → python_fastllm-0.0.64}/fastllm/openai_chat.py +26 -26
  12. {python_fastllm-0.0.63 → python_fastllm-0.0.64}/fastllm/openai_responses.py +36 -34
  13. {python_fastllm-0.0.63 → python_fastllm-0.0.64}/fastllm/responses.py +28 -28
  14. {python_fastllm-0.0.63 → python_fastllm-0.0.64}/fastllm/types.py +47 -32
  15. {python_fastllm-0.0.63 → python_fastllm-0.0.64}/python_fastllm.egg-info/PKG-INFO +12 -12
  16. {python_fastllm-0.0.63 → python_fastllm-0.0.64}/fastllm/streaming.py +0 -0
  17. {python_fastllm-0.0.63 → python_fastllm-0.0.64}/pyproject.toml +0 -0
  18. {python_fastllm-0.0.63 → python_fastllm-0.0.64}/python_fastllm.egg-info/SOURCES.txt +0 -0
  19. {python_fastllm-0.0.63 → python_fastllm-0.0.64}/python_fastllm.egg-info/dependency_links.txt +0 -0
  20. {python_fastllm-0.0.63 → python_fastllm-0.0.64}/python_fastllm.egg-info/entry_points.txt +0 -0
  21. {python_fastllm-0.0.63 → python_fastllm-0.0.64}/python_fastllm.egg-info/requires.txt +0 -0
  22. {python_fastllm-0.0.63 → python_fastllm-0.0.64}/python_fastllm.egg-info/top_level.txt +0 -0
  23. {python_fastllm-0.0.63 → python_fastllm-0.0.64}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-fastllm
3
- Version: 0.0.63
3
+ Version: 0.0.64
4
4
  Author-email: Kerem Turgutlu <keremturgutlu@gmail.com>
5
5
  License-Expression: Apache-2.0
6
6
  Project-URL: Repository, https://github.com/AnswerDotAI/fastllm
@@ -32,7 +32,7 @@ Run `pip install python-fastllm` or clone from github and install locally.
32
32
  ``` python
33
33
  from aidialog.msg_parts import Msg, Part, Text, Thinking, ToolUse, InputImage, mk_tool_res_msg, Completion
34
34
  from fastllm.acomplete import acomplete
35
- from fastllm.types import sample_img_url, sample_doms
35
+ from fastllm.types import sample_img_url, sample_doms, sonn, sonn46
36
36
  import asyncio, json
37
37
  ```
38
38
 
@@ -61,7 +61,7 @@ mtok = 1024
61
61
  Use the same `acomplete` interface with Claude, GPT, Gemini, or Kimi. Set `model` and any provider-specific routing arguments:
62
62
 
63
63
  ``` python
64
- models = [('claude-sonnet-5', {}), ('gpt-4o-mini', {}), ('models/gemini-3-flash-preview', {}),
64
+ models = [(sonn, {}), ('gpt-4o-mini', {}), ('models/gemini-3-flash-preview', {}),
65
65
  ('accounts/fireworks/models/kimi-k3', dict(vendor_name='fireworks_ai'))]
66
66
  for name, kw in models:
67
67
  r = await acomplete([user("Translate 'hello' into French. Return only the translation.")],
@@ -83,7 +83,7 @@ Pass `system` to supply a system prompt. `fastllm` translates it to the provider
83
83
  sys = "You are a pirate chef. Always respond in pirate speak and mention food. Use one short sentence."
84
84
 
85
85
  print("Claude: ", end='')
86
- r = await stream([user("What should I do today?")], model='claude-sonnet-5', system=sys, max_tokens=mtok)
86
+ r = await stream([user("What should I do today?")], model=sonn, system=sys, max_tokens=mtok)
87
87
 
88
88
  print("Gemini: ", end='')
89
89
  r = await stream([user("What should I do today?")], model='models/gemini-3-flash-preview', system=sys, max_tokens=mtok)
@@ -101,7 +101,7 @@ tools = [{"type": "function", "function": dict(name="get_weather", description="
101
101
  parameters=dict(type="object", properties={"city": {"type": "string"}}, required=["city"]))}]
102
102
 
103
103
  msgs = [user("What's the weather in Paris?")]
104
- r1 = await stream(msgs, model='claude-sonnet-5', tools=tools, max_tokens=mtok)
104
+ r1 = await stream(msgs, model=sonn, tools=tools, max_tokens=mtok)
105
105
  print("Tool calls:", r1.tool_calls)
106
106
  ```
107
107
 
@@ -112,7 +112,7 @@ print("Tool calls:", r1.tool_calls)
112
112
  ``` python
113
113
  msgs += [r1.message, mk_tool_res_msg(r1.tool_calls, ['22°C, sunny with light clouds']),
114
114
  user("Should I bring a jacket? Answer in one short sentence.")]
115
- r2 = await stream(msgs, model='claude-sonnet-5', tools=tools, max_tokens=mtok)
115
+ r2 = await stream(msgs, model=sonn, tools=tools, max_tokens=mtok)
116
116
  ```
117
117
 
118
118
  No, a jacket isn't necessary — it's a mild, sunny 22°C day.
@@ -122,10 +122,10 @@ r2 = await stream(msgs, model='claude-sonnet-5', tools=tools, max_tokens=mtok)
122
122
  Control whether the model must use tools, can’t use tools, or decides on its own:
123
123
 
124
124
  ``` python
125
- r = await acomplete([user('Hello there!')], model='claude-sonnet-5', tools=tools, tool_choice='required', max_tokens=mtok)
125
+ r = await acomplete([user('Hello there!')], model=sonn, tools=tools, tool_choice='required', max_tokens=mtok)
126
126
  print("Required:", [tc.name for tc in r.tool_calls])
127
127
 
128
- r = await acomplete([user("What's the weather?")], model='claude-sonnet-5', tools=tools, tool_choice='none', max_tokens=mtok)
128
+ r = await acomplete([user("What's the weather?")], model=sonn, tools=tools, tool_choice='none', max_tokens=mtok)
129
129
  print("None:", r.tool_calls)
130
130
  ```
131
131
 
@@ -138,7 +138,7 @@ Set `reasoning_effort` to `low`, `medium`, or `high` for supported models. The a
138
138
 
139
139
  ``` python
140
140
  print("Claude: ", end='')
141
- r = await stream([user("What is 127 × 849? Return just the number.")], model='claude-sonnet-4-6',
141
+ r = await stream([user("What is 127 × 849? Return just the number.")], model=sonn46,
142
142
  reasoning_effort='low', max_tokens=8192)
143
143
  for p in r.message.content:
144
144
  if isinstance(p, Thinking): print(f"Thinking excerpt: {p.text[:150]}...")
@@ -177,11 +177,11 @@ long_ctx = "You are an expert on the solar system. " * 200
177
177
  system = Text(long_ctx, cache_control={'type': 'ephemeral'})
178
178
 
179
179
  r1 = await acomplete([user("What is Jupiter's mass? Answer in one sentence.")],
180
- model='claude-sonnet-5', system=system, max_tokens=mtok)
180
+ model=sonn, system=system, max_tokens=mtok)
181
181
  print("First call: cache creation tokens", r1.usage.cache_creation_tokens)
182
182
 
183
183
  r2 = await acomplete([user("What is Saturn's mass? Answer in one sentence.")],
184
- model='claude-sonnet-5', system=system, max_tokens=mtok)
184
+ model=sonn, system=system, max_tokens=mtok)
185
185
  print("Second call: cache read tokens", r2.usage.cached_tokens)
186
186
  ```
187
187
 
@@ -195,7 +195,7 @@ Use `InputImage` with a model that supports images. The example sends the same i
195
195
  ``` python
196
196
  img_msg = Msg(role='user', content=[InputImage(sample_img_url), Text('List three visible objects, using nouns only.')])
197
197
 
198
- for name, kw in [('claude-sonnet-5', {}), ('gpt-4o-mini', {}), ('models/gemini-3-flash-preview', {})]:
198
+ for name, kw in [(sonn, {}), ('gpt-4o-mini', {}), ('models/gemini-3-flash-preview', {})]:
199
199
  print(f"{name}: ", end='')
200
200
  r = await stream([img_msg], model=name, max_tokens=mtok, **kw)
201
201
  ```
@@ -16,7 +16,7 @@ Run `pip install python-fastllm` or clone from github and install locally.
16
16
  ``` python
17
17
  from aidialog.msg_parts import Msg, Part, Text, Thinking, ToolUse, InputImage, mk_tool_res_msg, Completion
18
18
  from fastllm.acomplete import acomplete
19
- from fastllm.types import sample_img_url, sample_doms
19
+ from fastllm.types import sample_img_url, sample_doms, sonn, sonn46
20
20
  import asyncio, json
21
21
  ```
22
22
 
@@ -45,7 +45,7 @@ mtok = 1024
45
45
  Use the same `acomplete` interface with Claude, GPT, Gemini, or Kimi. Set `model` and any provider-specific routing arguments:
46
46
 
47
47
  ``` python
48
- models = [('claude-sonnet-5', {}), ('gpt-4o-mini', {}), ('models/gemini-3-flash-preview', {}),
48
+ models = [(sonn, {}), ('gpt-4o-mini', {}), ('models/gemini-3-flash-preview', {}),
49
49
  ('accounts/fireworks/models/kimi-k3', dict(vendor_name='fireworks_ai'))]
50
50
  for name, kw in models:
51
51
  r = await acomplete([user("Translate 'hello' into French. Return only the translation.")],
@@ -67,7 +67,7 @@ Pass `system` to supply a system prompt. `fastllm` translates it to the provider
67
67
  sys = "You are a pirate chef. Always respond in pirate speak and mention food. Use one short sentence."
68
68
 
69
69
  print("Claude: ", end='')
70
- r = await stream([user("What should I do today?")], model='claude-sonnet-5', system=sys, max_tokens=mtok)
70
+ r = await stream([user("What should I do today?")], model=sonn, system=sys, max_tokens=mtok)
71
71
 
72
72
  print("Gemini: ", end='')
73
73
  r = await stream([user("What should I do today?")], model='models/gemini-3-flash-preview', system=sys, max_tokens=mtok)
@@ -85,7 +85,7 @@ tools = [{"type": "function", "function": dict(name="get_weather", description="
85
85
  parameters=dict(type="object", properties={"city": {"type": "string"}}, required=["city"]))}]
86
86
 
87
87
  msgs = [user("What's the weather in Paris?")]
88
- r1 = await stream(msgs, model='claude-sonnet-5', tools=tools, max_tokens=mtok)
88
+ r1 = await stream(msgs, model=sonn, tools=tools, max_tokens=mtok)
89
89
  print("Tool calls:", r1.tool_calls)
90
90
  ```
91
91
 
@@ -96,7 +96,7 @@ print("Tool calls:", r1.tool_calls)
96
96
  ``` python
97
97
  msgs += [r1.message, mk_tool_res_msg(r1.tool_calls, ['22°C, sunny with light clouds']),
98
98
  user("Should I bring a jacket? Answer in one short sentence.")]
99
- r2 = await stream(msgs, model='claude-sonnet-5', tools=tools, max_tokens=mtok)
99
+ r2 = await stream(msgs, model=sonn, tools=tools, max_tokens=mtok)
100
100
  ```
101
101
 
102
102
  No, a jacket isn't necessary — it's a mild, sunny 22°C day.
@@ -106,10 +106,10 @@ r2 = await stream(msgs, model='claude-sonnet-5', tools=tools, max_tokens=mtok)
106
106
  Control whether the model must use tools, can’t use tools, or decides on its own:
107
107
 
108
108
  ``` python
109
- r = await acomplete([user('Hello there!')], model='claude-sonnet-5', tools=tools, tool_choice='required', max_tokens=mtok)
109
+ r = await acomplete([user('Hello there!')], model=sonn, tools=tools, tool_choice='required', max_tokens=mtok)
110
110
  print("Required:", [tc.name for tc in r.tool_calls])
111
111
 
112
- r = await acomplete([user("What's the weather?")], model='claude-sonnet-5', tools=tools, tool_choice='none', max_tokens=mtok)
112
+ r = await acomplete([user("What's the weather?")], model=sonn, tools=tools, tool_choice='none', max_tokens=mtok)
113
113
  print("None:", r.tool_calls)
114
114
  ```
115
115
 
@@ -122,7 +122,7 @@ Set `reasoning_effort` to `low`, `medium`, or `high` for supported models. The a
122
122
 
123
123
  ``` python
124
124
  print("Claude: ", end='')
125
- r = await stream([user("What is 127 × 849? Return just the number.")], model='claude-sonnet-4-6',
125
+ r = await stream([user("What is 127 × 849? Return just the number.")], model=sonn46,
126
126
  reasoning_effort='low', max_tokens=8192)
127
127
  for p in r.message.content:
128
128
  if isinstance(p, Thinking): print(f"Thinking excerpt: {p.text[:150]}...")
@@ -161,11 +161,11 @@ long_ctx = "You are an expert on the solar system. " * 200
161
161
  system = Text(long_ctx, cache_control={'type': 'ephemeral'})
162
162
 
163
163
  r1 = await acomplete([user("What is Jupiter's mass? Answer in one sentence.")],
164
- model='claude-sonnet-5', system=system, max_tokens=mtok)
164
+ model=sonn, system=system, max_tokens=mtok)
165
165
  print("First call: cache creation tokens", r1.usage.cache_creation_tokens)
166
166
 
167
167
  r2 = await acomplete([user("What is Saturn's mass? Answer in one sentence.")],
168
- model='claude-sonnet-5', system=system, max_tokens=mtok)
168
+ model=sonn, system=system, max_tokens=mtok)
169
169
  print("Second call: cache read tokens", r2.usage.cached_tokens)
170
170
  ```
171
171
 
@@ -179,7 +179,7 @@ Use `InputImage` with a model that supports images. The example sends the same i
179
179
  ``` python
180
180
  img_msg = Msg(role='user', content=[InputImage(sample_img_url), Text('List three visible objects, using nouns only.')])
181
181
 
182
- for name, kw in [('claude-sonnet-5', {}), ('gpt-4o-mini', {}), ('models/gemini-3-flash-preview', {})]:
182
+ for name, kw in [(sonn, {}), ('gpt-4o-mini', {}), ('models/gemini-3-flash-preview', {})]:
183
183
  print(f"{name}: ", end='')
184
184
  r = await stream([img_msg], model=name, max_tokens=mtok, **kw)
185
185
  ```
@@ -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.64"
@@ -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'),
@@ -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))
@@ -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,7 +9,7 @@ __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
14
  from collections import Counter
15
15
  from fastcore.utils import *
@@ -22,7 +22,7 @@ from aidialog.msg_parts import (MdStr, Part, PartType, mk_part, Msg, Completion,
22
22
  from .streaming import *
23
23
  from .streaming import mk_acollect_stream
24
24
 
25
- # %% ../nbs/04_anthropic.ipynb #5656d1d6
25
+ # %% ../nbs/06_anthropic.ipynb #5656d1d6
26
26
  ant_tc_types = ("tool_use", "server_tool_use", "mcp_tool_use")
27
27
 
28
28
  def norm_tool_call(b):
@@ -37,7 +37,7 @@ def norm_tool_calls(resp, delta=False):
37
37
  if tc:= norm_tool_call(b): out.append(tc)
38
38
  return out
39
39
 
40
- # %% ../nbs/04_anthropic.ipynb #0a1e2a8e
40
+ # %% ../nbs/06_anthropic.ipynb #0a1e2a8e
41
41
  def norm_usage(resp):
42
42
  "Normalize Anthropic usage shape."
43
43
  if not (usg:=resp.get("usage")): return None
@@ -58,7 +58,7 @@ def finalize_usage(usg, parts):
58
58
  return Usage(prompt_tokens=usg.prompt_tokens, completion_tokens=ct-rt, total_tokens=usg.prompt_tokens+ct,
59
59
  cached_tokens=usg.cached_tokens, cache_creation_tokens=usg.cache_creation_tokens, reasoning_tokens=rt, search_queries=usg.search_queries, raw=usg.raw)
60
60
 
61
- # %% ../nbs/04_anthropic.ipynb #7a8b1f8f
61
+ # %% ../nbs/06_anthropic.ipynb #7a8b1f8f
62
62
  def norm_finish(resp, tcs=None):
63
63
  "Canonicalize finish_reason to OpenAI Chat values: stop, tool_calls, length, content_filter."
64
64
  reason = resp.get("stop_reason")
@@ -66,7 +66,7 @@ def norm_finish(resp, tcs=None):
66
66
  r = mp.get(reason, reason)
67
67
  return FinishReason.tool_calls if r==FinishReason.stop and any(~L(tcs).attrgot('server')) else r
68
68
 
69
- # %% ../nbs/04_anthropic.ipynb #9796be28
69
+ # %% ../nbs/06_anthropic.ipynb #9796be28
70
70
  def search_results(parts, blocks):
71
71
  "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
72
  calls = {p.id: p for p in parts if isinstance(p, ToolUse) and p.server}
@@ -76,7 +76,7 @@ def search_results(parts, blocks):
76
76
  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
77
  return parts
78
78
 
79
- # %% ../nbs/04_anthropic.ipynb #20d2e803
79
+ # %% ../nbs/06_anthropic.ipynb #20d2e803
80
80
  def _ant_part_type(typ):
81
81
  "Map Anthropic content block type to canonical PartType."
82
82
  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 +97,12 @@ def norm_parts(resp):
97
97
  parts.append(Text(b.get("text", ""), citations=cites, raw=b))
98
98
  return search_results(parts, resp.get("content", []))
99
99
 
100
- # %% ../nbs/04_anthropic.ipynb #f24e52db
100
+ # %% ../nbs/06_anthropic.ipynb #f24e52db
101
101
  def raw_msg(resp):
102
102
  "The assistant message as the API sent it"
103
103
  return dict(role='assistant', content=resp['content'])
104
104
 
105
- # %% ../nbs/04_anthropic.ipynb #5d1440b7
105
+ # %% ../nbs/06_anthropic.ipynb #5d1440b7
106
106
  def norm_tr_parts(content):
107
107
  "Normalize an Anthropic `tool_result` content str or block list to canonical `Part`s"
108
108
  if isinstance(content, str): return [Text(content)]
@@ -118,7 +118,7 @@ def norm_tr_parts(content):
118
118
  else: parts.append(mk_part(**b))
119
119
  return parts
120
120
 
121
- # %% ../nbs/04_anthropic.ipynb #a3869e31
121
+ # %% ../nbs/06_anthropic.ipynb #a3869e31
122
122
  def norm_sse_event(ev, **kwargs):
123
123
  ev = obj2dict(ev)
124
124
  typ = ev.get("type")
@@ -142,12 +142,12 @@ def norm_sse_event(ev, **kwargs):
142
142
  elif typ == "error": raise api_error_from_event(ev)
143
143
  return Delta(text=text, thinking=thinking, tool_calls=tcs, citations=citations, raw=ev, **kwargs)
144
144
 
145
- # %% ../nbs/04_anthropic.ipynb #33cd3bd3
145
+ # %% ../nbs/06_anthropic.ipynb #33cd3bd3
146
146
  def delta_index_fn(d, typ, last_typ, last_idx):
147
147
  'Returns accumulation index for current delta and updated last idx'
148
148
  return nested_idx(d, 'raw', 'index'), None
149
149
 
150
- # %% ../nbs/04_anthropic.ipynb #de900e34
150
+ # %% ../nbs/06_anthropic.ipynb #de900e34
151
151
  def collate_raw(deltas):
152
152
  "The assistant message rebuilt from a stream's events"
153
153
  blocks = {}
@@ -164,7 +164,7 @@ def collate_raw(deltas):
164
164
  elif typ == 'content_block_stop' and '_json' in blocks[i]: blocks[i]['input'] = json.loads(blocks[i].pop('_json') or '{}')
165
165
  return dict(role='assistant', content=list(blocks.values()))
166
166
 
167
- # %% ../nbs/04_anthropic.ipynb #c994adb1
167
+ # %% ../nbs/06_anthropic.ipynb #c994adb1
168
168
  @delegates(mk_acollect_stream, but=['index_fn', 'api_name'])
169
169
  async def acollect_stream(resp, **kwargs):
170
170
  res = mk_acollect_stream(norm_and_yield(resp, norm_sse_event), index_fn=delta_index_fn, api_name='anthropic', **kwargs)
@@ -172,16 +172,16 @@ async def acollect_stream(resp, **kwargs):
172
172
  if isinstance(o, Completion): search_results(o.message.content, o.message.raw['content'])
173
173
  yield o
174
174
 
175
- # %% ../nbs/04_anthropic.ipynb #bb11ab62
175
+ # %% ../nbs/06_anthropic.ipynb #bb11ab62
176
176
  def _ant_cc(block, p):
177
177
  "Add the part's prompt-cache directive to `block`, if it has one."
178
178
  if p.cache_control: block['cache_control'] = p.cache_control
179
179
  return block
180
180
 
181
- # %% ../nbs/04_anthropic.ipynb #62e9a042
181
+ # %% ../nbs/06_anthropic.ipynb #62e9a042
182
182
  def _sanid(id_str): return re.sub(r'[^a-zA-Z0-9_-]', '_', id_str or '')
183
183
 
184
- # %% ../nbs/04_anthropic.ipynb #6ec772cb
184
+ # %% ../nbs/06_anthropic.ipynb #6ec772cb
185
185
  def denorm_tool_use(p:ToolUse):
186
186
  "Convert canonical tool_use Part to Anthropic tool_use content block."
187
187
  return _ant_cc(dict(type='tool_use', id=_sanid(p.id or ''), name=p.name or '', input=p.arguments or {}), p)
@@ -209,7 +209,7 @@ def denorm_msgs(msgs:list[Msg]):
209
209
  elif m.role == 'tool': res.append(denorm_tool(m))
210
210
  return res
211
211
 
212
- # %% ../nbs/04_anthropic.ipynb #f762a6f2
212
+ # %% ../nbs/06_anthropic.ipynb #f762a6f2
213
213
  def denorm_tool_schs(tools):
214
214
  "Convert canonical tools to Anthropic format."
215
215
  out = []
@@ -220,7 +220,7 @@ def denorm_tool_schs(tools):
220
220
  out.append(dict(name=name, description=desc, input_schema=params))
221
221
  return out
222
222
 
223
- # %% ../nbs/04_anthropic.ipynb #6118d7ea
223
+ # %% ../nbs/06_anthropic.ipynb #6118d7ea
224
224
  def denorm_tool_choice(v):
225
225
  "Map canonical tool_choice to Anthropic format."
226
226
  if v is None: return None
@@ -229,7 +229,7 @@ def denorm_tool_choice(v):
229
229
  if v in ('none', 'off', 'disabled'): return {'type': 'none'}
230
230
  return {'type': 'tool', 'name': v}
231
231
 
232
- # %% ../nbs/04_anthropic.ipynb #310f1e75
232
+ # %% ../nbs/06_anthropic.ipynb #310f1e75
233
233
  def denorm_reasoning(v):
234
234
  "Map canonical reasoning_effort to Anthropic adaptive thinking + output_config."
235
235
  mp = dict(minimal='low', low='low', medium='medium', high='high', xhigh='xhigh', max='max')
@@ -239,7 +239,7 @@ def denorm_reasoning(v):
239
239
  elif isinstance(v, str) and v in mp: return {"thinking": {"type": "adaptive"}, "output_config": {"effort": mp.get(v)}}
240
240
  raise err
241
241
 
242
- # %% ../nbs/04_anthropic.ipynb #8fa9fbb8
242
+ # %% ../nbs/06_anthropic.ipynb #8fa9fbb8
243
243
  def denorm_web_search(v):
244
244
  "Map canonical web_search_options to Anthropic hosted web_search tool."
245
245
  _max_uses = dict(low=1, medium=5, high=10)
@@ -249,13 +249,13 @@ def denorm_web_search(v):
249
249
  if (u := (v or {}).get("user_location", {}).get("approximate")): t["user_location"] = {"type": "approximate", **u}
250
250
  return t
251
251
 
252
- # %% ../nbs/04_anthropic.ipynb #6b485275
252
+ # %% ../nbs/06_anthropic.ipynb #6b485275
253
253
  def denorm_system(sp):
254
254
  if isinstance(sp, Msg): sp = sp.content[0]
255
255
  if isinstance(sp, Part): return [_ant_cc(dict(type='text', text=sp.text), sp)]
256
256
  else: return sp
257
257
 
258
- # %% ../nbs/04_anthropic.ipynb #1b990cc9
258
+ # %% ../nbs/06_anthropic.ipynb #1b990cc9
259
259
  def denorm_user(m:Msg):
260
260
  "Convert canonical user Msg to Anthropic user message."
261
261
  parts = []
@@ -267,19 +267,19 @@ def denorm_user(m:Msg):
267
267
  elif isinstance(p, InputFile): parts.append(_ant_cc(denorm_file(p), p))
268
268
  return dict(role='user', content=parts)
269
269
 
270
- # %% ../nbs/04_anthropic.ipynb #edd87272
270
+ # %% ../nbs/06_anthropic.ipynb #edd87272
271
271
  def denorm_image(p, max_sz=None):
272
272
  if (b64:=data_url(p.text)):
273
273
  data = resize_b64(b64[1], max_sz) if max_sz else b64[1]
274
274
  return {"type": "image", "source": dict(type="base64", media_type=b64[0], data=data)}
275
275
  return {"type": "image", "source": {"type": "url", "url": p.text}}
276
276
 
277
- # %% ../nbs/04_anthropic.ipynb #fc6bbdfc
277
+ # %% ../nbs/06_anthropic.ipynb #fc6bbdfc
278
278
  def denorm_file(p):
279
279
  if (b64:=data_url(p.text)): return {"type": "document", "source": dict(type="base64", media_type=b64[0], data=b64[1])}
280
280
  return {"type": "document", "source": {"type": "url", "url": p.text}}
281
281
 
282
- # %% ../nbs/04_anthropic.ipynb #37b63cc2
282
+ # %% ../nbs/06_anthropic.ipynb #37b63cc2
283
283
  def denorm_tool_result(p:ToolResult):
284
284
  "Convert canonical tool_result Part to Anthropic tool_result content block."
285
285
  tid = _sanid(p.id or (p.raw or {}).get('call_id',''))
@@ -293,7 +293,7 @@ def denorm_tool_result(p:ToolResult):
293
293
  return _ant_cc(dict(type='tool_result', tool_use_id=tid, content=blocks), p)
294
294
  return _ant_cc(dict(type='tool_result', tool_use_id=tid, content=tool_text(p.text)), p)
295
295
 
296
- # %% ../nbs/04_anthropic.ipynb #e4eb8d3b
296
+ # %% ../nbs/06_anthropic.ipynb #e4eb8d3b
297
297
  def _add_cache_control(
298
298
  msg, # Canonical Msg to mark
299
299
  ttl=None # Cache TTL: '5m' (default) or '1h'
@@ -304,7 +304,7 @@ def _add_cache_control(
304
304
  if idxs: msg.content[idxs[-1]].cache_control = cc
305
305
  return msg
306
306
 
307
- # %% ../nbs/04_anthropic.ipynb #6bc52ef5
307
+ # %% ../nbs/06_anthropic.ipynb #6bc52ef5
308
308
  def _clear_cache_control(obj):
309
309
  "Clear canonical cache marks from a message or part."
310
310
  if isinstance(obj, Msg):
@@ -332,7 +332,7 @@ def apply_cache_idxs(
332
332
  except IndexError: continue
333
333
  return sp
334
334
 
335
- # %% ../nbs/04_anthropic.ipynb #587d4fe5
335
+ # %% ../nbs/06_anthropic.ipynb #587d4fe5
336
336
  @delegates(payload_kwargs)
337
337
  def mk_payload(msgs, model, **kwargs):
338
338
  sp = kwargs.get('system')
@@ -347,7 +347,7 @@ def mk_payload(msgs, model, **kwargs):
347
347
  if (temp:=kwargs.get('temperature')) is not None: payload['temperature'] = temp
348
348
  return payload
349
349
 
350
- # %% ../nbs/04_anthropic.ipynb #60d52c1f
350
+ # %% ../nbs/06_anthropic.ipynb #60d52c1f
351
351
  def get_hdrs(api_key=None, oauth_token=None):
352
352
  "An OAuth token travels as a bearer under Anthropic's OAuth beta; an API key as `x-api-key`"
353
353
  hdrs = {"anthropic-version": "2023-06-01"}
@@ -355,7 +355,7 @@ def get_hdrs(api_key=None, oauth_token=None):
355
355
  return hdrs | {"x-api-key": get_api_key(api_key, 'ANTHROPIC_API_KEY')}
356
356
 
357
357
 
358
- # %% ../nbs/04_anthropic.ipynb #0d03643a
358
+ # %% ../nbs/06_anthropic.ipynb #0d03643a
359
359
  def cost(usage, m):
360
360
  raw = usage.raw
361
361
  in_tok = raw['input_tokens']
@@ -370,7 +370,7 @@ def cost(usage, m):
370
370
  cost += cache_1h * m.get('cache_creation_input_token_cost_above_1hr', 0)
371
371
  return cost
372
372
 
373
- # %% ../nbs/04_anthropic.ipynb #f7c0b989
373
+ # %% ../nbs/06_anthropic.ipynb #f7c0b989
374
374
  api_registry.register('anthropic', norm_tool_calls=norm_tool_calls, norm_parts=norm_parts, norm_finish=norm_finish, norm_usage=norm_usage,
375
375
  finalize_usage=finalize_usage, raw_msg=raw_msg, collate_raw=collate_raw, acollect_stream=acollect_stream, mk_payload=mk_payload,
376
376
  cost=cost, get_hdrs=get_hdrs, endpoint='/v1/messages')