python-fastllm 0.0.60__tar.gz → 0.0.62__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.60 → python_fastllm-0.0.62}/PKG-INFO +16 -29
  2. {python_fastllm-0.0.60 → python_fastllm-0.0.62}/README.md +15 -28
  3. {python_fastllm-0.0.60 → python_fastllm-0.0.62}/fastllm/__init__.py +1 -1
  4. {python_fastllm-0.0.60 → python_fastllm-0.0.62}/fastllm/acomplete.py +8 -4
  5. {python_fastllm-0.0.60 → python_fastllm-0.0.62}/fastllm/chat.py +4 -0
  6. {python_fastllm-0.0.60 → python_fastllm-0.0.62}/fastllm/model_prices.json +6517 -470
  7. {python_fastllm-0.0.60 → python_fastllm-0.0.62}/fastllm/openai_chat.py +1 -1
  8. {python_fastllm-0.0.60 → python_fastllm-0.0.62}/fastllm/openai_responses.py +1 -0
  9. {python_fastllm-0.0.60 → python_fastllm-0.0.62}/fastllm/responses.py +10 -7
  10. {python_fastllm-0.0.60 → python_fastllm-0.0.62}/fastllm/types.py +28 -31
  11. {python_fastllm-0.0.60 → python_fastllm-0.0.62}/python_fastllm.egg-info/PKG-INFO +16 -29
  12. {python_fastllm-0.0.60 → python_fastllm-0.0.62}/fastllm/_modidx.py +0 -0
  13. {python_fastllm-0.0.60 → python_fastllm-0.0.62}/fastllm/anthropic.py +0 -0
  14. {python_fastllm-0.0.60 → python_fastllm-0.0.62}/fastllm/codex.py +0 -0
  15. {python_fastllm-0.0.60 → python_fastllm-0.0.62}/fastllm/gemini.py +0 -0
  16. {python_fastllm-0.0.60 → python_fastllm-0.0.62}/fastllm/streaming.py +0 -0
  17. {python_fastllm-0.0.60 → python_fastllm-0.0.62}/pyproject.toml +0 -0
  18. {python_fastllm-0.0.60 → python_fastllm-0.0.62}/python_fastllm.egg-info/SOURCES.txt +0 -0
  19. {python_fastllm-0.0.60 → python_fastllm-0.0.62}/python_fastllm.egg-info/dependency_links.txt +0 -0
  20. {python_fastllm-0.0.60 → python_fastllm-0.0.62}/python_fastllm.egg-info/entry_points.txt +0 -0
  21. {python_fastllm-0.0.60 → python_fastllm-0.0.62}/python_fastllm.egg-info/requires.txt +0 -0
  22. {python_fastllm-0.0.60 → python_fastllm-0.0.62}/python_fastllm.egg-info/top_level.txt +0 -0
  23. {python_fastllm-0.0.60 → python_fastllm-0.0.62}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-fastllm
3
- Version: 0.0.60
3
+ Version: 0.0.62
4
4
  Author-email: Kerem Turgutlu <keremturgutlu@gmail.com>
5
5
  License: Apache-2.0
6
6
  Project-URL: Repository, https://github.com/AnswerDotAI/fastllm
@@ -61,15 +61,11 @@ 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 = [
65
- ('claude-sonnet-5', {}),
66
- ('gpt-4o-mini', {}),
67
- ('models/gemini-3-flash-preview', {}),
68
- ('accounts/fireworks/models/kimi-k3', dict(vendor_name='fireworks_ai'))
69
- ]
64
+ models = [('claude-sonnet-5', {}), ('gpt-4o-mini', {}), ('models/gemini-3-flash-preview', {}),
65
+ ('accounts/fireworks/models/kimi-k3', dict(vendor_name='fireworks_ai'))]
70
66
  for name, kw in models:
71
67
  r = await acomplete([user("Translate 'hello' into French. Return only the translation.")],
72
- model=name, max_tokens=mtok, **kw)
68
+ model=name, max_tokens=mtok, **kw)
73
69
  text = ''.join(p.text for p in r.message.content if isinstance(p, Text))
74
70
  print(f"{name}: {text.strip()}")
75
71
  ```
@@ -101,11 +97,8 @@ r = await stream([user("What should I do today?")], model='models/gemini-3-flash
101
97
  Define tools in the shared schema. `fastllm` translates it to the provider’s tool format. This example handles a tool request, adds the result to the history, and continues the conversation:
102
98
 
103
99
  ``` python
104
- tools = [{"type": "function", "function": {
105
- "name": "get_weather",
106
- "description": "Get current weather for a city",
107
- "parameters": {"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"]}
108
- }}]
100
+ tools = [{"type": "function", "function": dict(name="get_weather", description="Get current weather for a city",
101
+ parameters=dict(type="object", properties={"city": {"type": "string"}}, required=["city"]))}]
109
102
 
110
103
  msgs = [user("What's the weather in Paris?")]
111
104
  r1 = await stream(msgs, model='claude-sonnet-5', tools=tools, max_tokens=mtok)
@@ -118,7 +111,7 @@ print("Tool calls:", r1.tool_calls)
118
111
 
119
112
  ``` python
120
113
  msgs += [r1.message, mk_tool_res_msg(r1.tool_calls, ['22°C, sunny with light clouds']),
121
- user("Should I bring a jacket? Answer in one short sentence.")]
114
+ user("Should I bring a jacket? Answer in one short sentence.")]
122
115
  r2 = await stream(msgs, model='claude-sonnet-5', tools=tools, max_tokens=mtok)
123
116
  ```
124
117
 
@@ -129,12 +122,10 @@ r2 = await stream(msgs, model='claude-sonnet-5', tools=tools, max_tokens=mtok)
129
122
  Control whether the model must use tools, can’t use tools, or decides on its own:
130
123
 
131
124
  ``` python
132
- r = await acomplete([user("Hello there!")], model='claude-sonnet-5',
133
- tools=tools, tool_choice='required', max_tokens=mtok)
125
+ r = await acomplete([user('Hello there!')], model='claude-sonnet-5', tools=tools, tool_choice='required', max_tokens=mtok)
134
126
  print("Required:", [tc.name for tc in r.tool_calls])
135
127
 
136
- r = await acomplete([user("What's the weather?")], model='claude-sonnet-5',
137
- tools=tools, tool_choice='none', max_tokens=mtok)
128
+ r = await acomplete([user("What's the weather?")], model='claude-sonnet-5', tools=tools, tool_choice='none', max_tokens=mtok)
138
129
  print("None:", r.tool_calls)
139
130
  ```
140
131
 
@@ -148,14 +139,13 @@ Set `reasoning_effort` to `low`, `medium`, or `high` for supported models. The a
148
139
  ``` python
149
140
  print("Claude: ", end='')
150
141
  r = await stream([user("What is 127 × 849? Return just the number.")], model='claude-sonnet-4-6',
151
- reasoning_effort='low', max_tokens=8192)
142
+ reasoning_effort='low', max_tokens=8192)
152
143
  for p in r.message.content:
153
144
  if isinstance(p, Thinking): print(f"Thinking excerpt: {p.text[:150]}...")
154
145
 
155
146
  print("Kimi: ", end='')
156
- r = await stream([user("What is 127 × 849? Return just the number.")],
157
- model='accounts/fireworks/models/kimi-k3', vendor_name='fireworks_ai',
158
- reasoning_effort='low', max_tokens=8192)
147
+ r = await stream([user('What is 127 × 849? Return just the number.')], model='accounts/fireworks/models/kimi-k3',
148
+ vendor_name='fireworks_ai', reasoning_effort='low', max_tokens=8192)
159
149
  for p in r.message.content:
160
150
  if isinstance(p, Thinking): print(f"Thinking excerpt: {p.text[:150]}...")
161
151
  ```
@@ -172,7 +162,7 @@ OpenAI’s Responses API supports server-side web search. Server tool calls are
172
162
  ``` python
173
163
  ws_tools = [{"type": "web_search_preview"}]
174
164
  r = await acomplete([user("What is the latest Python release? Reply with the version number only.")],
175
- model='gpt-4o-mini', tools=ws_tools, max_tokens=512)
165
+ model='gpt-4o-mini', tools=ws_tools, max_tokens=512)
176
166
  print("Server tools used:", [tc.name for tc in r.tool_calls if tc.server])
177
167
  ```
178
168
 
@@ -187,11 +177,11 @@ long_ctx = "You are an expert on the solar system. " * 200
187
177
  system = Text(long_ctx, cache_control={'type': 'ephemeral'})
188
178
 
189
179
  r1 = await acomplete([user("What is Jupiter's mass? Answer in one sentence.")],
190
- model='claude-sonnet-5', system=system, max_tokens=mtok)
180
+ model='claude-sonnet-5', system=system, max_tokens=mtok)
191
181
  print("First call: cache creation tokens", r1.usage.cache_creation_tokens)
192
182
 
193
183
  r2 = await acomplete([user("What is Saturn's mass? Answer in one sentence.")],
194
- model='claude-sonnet-5', system=system, max_tokens=mtok)
184
+ model='claude-sonnet-5', system=system, max_tokens=mtok)
195
185
  print("Second call: cache read tokens", r2.usage.cached_tokens)
196
186
  ```
197
187
 
@@ -203,10 +193,7 @@ print("Second call: cache read tokens", r2.usage.cached_tokens)
203
193
  Use `InputImage` with a model that supports images. The example sends the same image message to three providers:
204
194
 
205
195
  ``` python
206
- img_msg = Msg(role='user', content=[
207
- InputImage(sample_img_url),
208
- Text("List three visible objects, using nouns only.")
209
- ])
196
+ img_msg = Msg(role='user', content=[InputImage(sample_img_url), Text('List three visible objects, using nouns only.')])
210
197
 
211
198
  for name, kw in [('claude-sonnet-5', {}), ('gpt-4o-mini', {}), ('models/gemini-3-flash-preview', {})]:
212
199
  print(f"{name}: ", end='')
@@ -45,15 +45,11 @@ 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 = [
49
- ('claude-sonnet-5', {}),
50
- ('gpt-4o-mini', {}),
51
- ('models/gemini-3-flash-preview', {}),
52
- ('accounts/fireworks/models/kimi-k3', dict(vendor_name='fireworks_ai'))
53
- ]
48
+ models = [('claude-sonnet-5', {}), ('gpt-4o-mini', {}), ('models/gemini-3-flash-preview', {}),
49
+ ('accounts/fireworks/models/kimi-k3', dict(vendor_name='fireworks_ai'))]
54
50
  for name, kw in models:
55
51
  r = await acomplete([user("Translate 'hello' into French. Return only the translation.")],
56
- model=name, max_tokens=mtok, **kw)
52
+ model=name, max_tokens=mtok, **kw)
57
53
  text = ''.join(p.text for p in r.message.content if isinstance(p, Text))
58
54
  print(f"{name}: {text.strip()}")
59
55
  ```
@@ -85,11 +81,8 @@ r = await stream([user("What should I do today?")], model='models/gemini-3-flash
85
81
  Define tools in the shared schema. `fastllm` translates it to the provider’s tool format. This example handles a tool request, adds the result to the history, and continues the conversation:
86
82
 
87
83
  ``` python
88
- tools = [{"type": "function", "function": {
89
- "name": "get_weather",
90
- "description": "Get current weather for a city",
91
- "parameters": {"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"]}
92
- }}]
84
+ tools = [{"type": "function", "function": dict(name="get_weather", description="Get current weather for a city",
85
+ parameters=dict(type="object", properties={"city": {"type": "string"}}, required=["city"]))}]
93
86
 
94
87
  msgs = [user("What's the weather in Paris?")]
95
88
  r1 = await stream(msgs, model='claude-sonnet-5', tools=tools, max_tokens=mtok)
@@ -102,7 +95,7 @@ print("Tool calls:", r1.tool_calls)
102
95
 
103
96
  ``` python
104
97
  msgs += [r1.message, mk_tool_res_msg(r1.tool_calls, ['22°C, sunny with light clouds']),
105
- user("Should I bring a jacket? Answer in one short sentence.")]
98
+ user("Should I bring a jacket? Answer in one short sentence.")]
106
99
  r2 = await stream(msgs, model='claude-sonnet-5', tools=tools, max_tokens=mtok)
107
100
  ```
108
101
 
@@ -113,12 +106,10 @@ r2 = await stream(msgs, model='claude-sonnet-5', tools=tools, max_tokens=mtok)
113
106
  Control whether the model must use tools, can’t use tools, or decides on its own:
114
107
 
115
108
  ``` python
116
- r = await acomplete([user("Hello there!")], model='claude-sonnet-5',
117
- tools=tools, tool_choice='required', max_tokens=mtok)
109
+ r = await acomplete([user('Hello there!')], model='claude-sonnet-5', tools=tools, tool_choice='required', max_tokens=mtok)
118
110
  print("Required:", [tc.name for tc in r.tool_calls])
119
111
 
120
- r = await acomplete([user("What's the weather?")], model='claude-sonnet-5',
121
- tools=tools, tool_choice='none', max_tokens=mtok)
112
+ r = await acomplete([user("What's the weather?")], model='claude-sonnet-5', tools=tools, tool_choice='none', max_tokens=mtok)
122
113
  print("None:", r.tool_calls)
123
114
  ```
124
115
 
@@ -132,14 +123,13 @@ Set `reasoning_effort` to `low`, `medium`, or `high` for supported models. The a
132
123
  ``` python
133
124
  print("Claude: ", end='')
134
125
  r = await stream([user("What is 127 × 849? Return just the number.")], model='claude-sonnet-4-6',
135
- reasoning_effort='low', max_tokens=8192)
126
+ reasoning_effort='low', max_tokens=8192)
136
127
  for p in r.message.content:
137
128
  if isinstance(p, Thinking): print(f"Thinking excerpt: {p.text[:150]}...")
138
129
 
139
130
  print("Kimi: ", end='')
140
- r = await stream([user("What is 127 × 849? Return just the number.")],
141
- model='accounts/fireworks/models/kimi-k3', vendor_name='fireworks_ai',
142
- reasoning_effort='low', max_tokens=8192)
131
+ r = await stream([user('What is 127 × 849? Return just the number.')], model='accounts/fireworks/models/kimi-k3',
132
+ vendor_name='fireworks_ai', reasoning_effort='low', max_tokens=8192)
143
133
  for p in r.message.content:
144
134
  if isinstance(p, Thinking): print(f"Thinking excerpt: {p.text[:150]}...")
145
135
  ```
@@ -156,7 +146,7 @@ OpenAI’s Responses API supports server-side web search. Server tool calls are
156
146
  ``` python
157
147
  ws_tools = [{"type": "web_search_preview"}]
158
148
  r = await acomplete([user("What is the latest Python release? Reply with the version number only.")],
159
- model='gpt-4o-mini', tools=ws_tools, max_tokens=512)
149
+ model='gpt-4o-mini', tools=ws_tools, max_tokens=512)
160
150
  print("Server tools used:", [tc.name for tc in r.tool_calls if tc.server])
161
151
  ```
162
152
 
@@ -171,11 +161,11 @@ long_ctx = "You are an expert on the solar system. " * 200
171
161
  system = Text(long_ctx, cache_control={'type': 'ephemeral'})
172
162
 
173
163
  r1 = await acomplete([user("What is Jupiter's mass? Answer in one sentence.")],
174
- model='claude-sonnet-5', system=system, max_tokens=mtok)
164
+ model='claude-sonnet-5', system=system, max_tokens=mtok)
175
165
  print("First call: cache creation tokens", r1.usage.cache_creation_tokens)
176
166
 
177
167
  r2 = await acomplete([user("What is Saturn's mass? Answer in one sentence.")],
178
- model='claude-sonnet-5', system=system, max_tokens=mtok)
168
+ model='claude-sonnet-5', system=system, max_tokens=mtok)
179
169
  print("Second call: cache read tokens", r2.usage.cached_tokens)
180
170
  ```
181
171
 
@@ -187,10 +177,7 @@ print("Second call: cache read tokens", r2.usage.cached_tokens)
187
177
  Use `InputImage` with a model that supports images. The example sends the same image message to three providers:
188
178
 
189
179
  ``` python
190
- img_msg = Msg(role='user', content=[
191
- InputImage(sample_img_url),
192
- Text("List three visible objects, using nouns only.")
193
- ])
180
+ img_msg = Msg(role='user', content=[InputImage(sample_img_url), Text('List three visible objects, using nouns only.')])
194
181
 
195
182
  for name, kw in [('claude-sonnet-5', {}), ('gpt-4o-mini', {}), ('models/gemini-3-flash-preview', {})]:
196
183
  print(f"{name}: ", end='')
@@ -2,4 +2,4 @@
2
2
 
3
3
  - `fastllm.responses`: Provider-independent Responses resources and streaming over FastLLM"""
4
4
 
5
- __version__ = "0.0.60"
5
+ __version__ = "0.0.62"
@@ -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"),
@@ -167,20 +168,23 @@ async def acomplete(msgs, model, api_name=None, vendor_name=None, api_key=None,
167
168
  if own:
168
169
  vendor_name = ifnone(vendor_name, api_name) # an own-transport api: no HTTP client to build
169
170
  if oauth_token: kwargs['oauth_token'] = oauth_token # its `mk_payload` applies the user's token itself
170
- 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, xtra_hdrs=xtra_hdrs)
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)
171
172
  api = api_registry[api_name]
172
173
  if previous_response_id is not None:
173
174
  if not getattr(api, 'supports_previous_response_id', False):
174
175
  raise ValueError('previous_response_id is not supported by this transport')
175
176
  kwargs['previous_response_id'] = previous_response_id
176
177
  payload = api.mk_payload(msgs, model, stream=stream, **kwargs)
177
- xtra_body = ifnone(xtra_body, {})
178
178
  if fix := getattr(api, 'fix_payload', None): fix(payload, model, vendor_name)
179
+ body = payload | ifnone(xtra_body, {})
179
180
  if not own:
181
+ 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}
180
184
  ep = endpoint or api.endpoint
181
185
  path,params = ep(model, stream) if callable(ep) else (ep, None)
182
- if defaults.debug_mode: _debug_print(model, api_name, vendor_name, payload | xtra_body, None if own else cli._url(path))
183
- async def _call(): return await provider_req(cli, path, payload | xtra_body, params=params, stream=stream)
186
+ if defaults.debug_mode: _debug_print(model, api_name, vendor_name, body, None if own else cli._url(path))
187
+ async def _call(): return await provider_req(cli, path, body, params=params, stream=stream, headers=headers)
184
188
  async def _mk_gen():
185
189
  resp = payload if own else await _call() # an own-transport api's `acollect_stream` makes the request itself, from the payload
186
190
  async for o in api.acollect_stream(resp, model=model, vendor_name=vendor_name, stop_callables=stop_callables): yield o
@@ -9,6 +9,7 @@ __all__ = ['effort', 'contents', 'stop_reason', 'cite_footnote', 'postproc', 'Re
9
9
  'StopSequencesCallback', 'StreamAccum', 'adisplay_stream']
10
10
 
11
11
  # %% ../nbs/07_chat.ipynb #d5a3bc1f
12
+ import secrets
12
13
  from typing import Optional,Callable
13
14
  from fastcore.funccall import mk_ns, call_func, call_func_async, get_schema
14
15
  from fastcore.utils import *
@@ -168,6 +169,7 @@ class AsyncChat:
168
169
  cache=False, # Anthropic prompt caching
169
170
  cache_idxs:list=[-1], # Anthropic cache breakpoint idxs, use `0` for sys prompt if provided
170
171
  ttl=None, # Anthropic prompt caching ttl
172
+ prompt_cache_key=None, # Stable conversation key for OpenAI Responses; generated once if omitted
171
173
  api_name=None, # API to use, one of ApiName: openai (responses), openai_chat, anthropic, gemini
172
174
  vendor_name=None, # Vendor name, one of vendor_mapping which resolves api_base/api_key automatically
173
175
  api_key=None, # API key when model can't be resolved or vendor_name is not known or codex
@@ -195,6 +197,7 @@ class AsyncChat:
195
197
  self.response_id,self._response_hist_idx = None,0
196
198
  self._turn_start = 0 # index into `hist` where the current turn began, for `full`
197
199
  self.last_req_use = None # usage of the latest request only; `use` accumulates across a turn's tool-call steps
200
+ if prompt_cache_key is None: prompt_cache_key = secrets.token_hex(16)
198
201
  store_attr(but='cbs')
199
202
  self.cbs = L()
200
203
  if default_cbs: self.add_cbs(defaults.chat_callbacks)
@@ -256,6 +259,7 @@ def _prep_call(self:AsyncChat, search, max_tokens, kwargs, stream=False, think=N
256
259
  if self.base_url: kwargs['base_url'] = self.base_url
257
260
  if self.endpoint: kwargs['endpoint'] = self.endpoint
258
261
  if self.extra_headers: kwargs['xtra_hdrs'] = self.extra_headers
262
+ kwargs.setdefault('prompt_cache_key', self.prompt_cache_key)
259
263
  kwargs.update(_think_kw(self.model, think, self.vendor_name))
260
264
  return max_tokens
261
265