python-fastllm 0.0.61__py3-none-any.whl → 0.0.63__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.61"
5
+ __version__ = "0.0.63"
fastllm/acomplete.py CHANGED
@@ -168,20 +168,23 @@ async def acomplete(msgs, model, api_name=None, vendor_name=None, api_key=None,
168
168
  if own:
169
169
  vendor_name = ifnone(vendor_name, api_name) # an own-transport api: no HTTP client to build
170
170
  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, 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)
172
172
  api = api_registry[api_name]
173
173
  if previous_response_id is not None:
174
174
  if not getattr(api, 'supports_previous_response_id', False):
175
175
  raise ValueError('previous_response_id is not supported by this transport')
176
176
  kwargs['previous_response_id'] = previous_response_id
177
177
  payload = api.mk_payload(msgs, model, stream=stream, **kwargs)
178
- xtra_body = ifnone(xtra_body, {})
179
178
  if fix := getattr(api, 'fix_payload', None): fix(payload, model, vendor_name)
179
+ body = payload | ifnone(xtra_body, {})
180
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}
181
184
  ep = endpoint or api.endpoint
182
185
  path,params = ep(model, stream) if callable(ep) else (ep, None)
183
- if defaults.debug_mode: _debug_print(model, api_name, vendor_name, payload | xtra_body, None if own else cli._url(path))
184
- 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)
185
188
  async def _mk_gen():
186
189
  resp = payload if own else await _call() # an own-transport api's `acollect_stream` makes the request itself, from the payload
187
190
  async for o in api.acollect_stream(resp, model=model, vendor_name=vendor_name, stop_callables=stop_callables): yield o
fastllm/chat.py CHANGED
@@ -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
 
@@ -257,6 +257,7 @@ def mk_payload(msgs, model, **kwargs):
257
257
  if tools:=kwargs.get('tools'): payload['tools'] = denorm_tool_schs(tools)
258
258
  if tchc:=kwargs.get('tool_choice'): payload['tool_choice'] = denorm_tool_choice(tchc)
259
259
  if rid:=kwargs.get('previous_response_id'): payload['previous_response_id'] = rid
260
+ if key:=kwargs.get('prompt_cache_key'): payload['prompt_cache_key'] = key
260
261
  if (ptc:=kwargs.get('parallel_tool_calls')) is not None: payload['parallel_tool_calls'] = ptc
261
262
  if thk:=kwargs.get('reasoning_effort'): payload['reasoning'] = denorm_reasoning(thk)
262
263
  if (wopts:=kwargs.get('web_search_options')) is not None: payload.setdefault('tools', []).append(denorm_web_search(wopts))
fastllm/responses.py CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  `normalize_call_ids` supplies missing client call IDs before we retain the history. Later tool results can refer to the same IDs that the client received.
6
6
 
7
- `ResponseState` holds the public ID, model, full message history, and optional provider response ID. The host can store it under its own ownership and expiry rules. A provider ID lets the next call send just the new messages; without one, it needs the retained history.
7
+ `ResponseState` holds the public ID, model, full message history, prompt cache key, and optional provider response ID. The host can store it under its own ownership and expiry rules. A provider ID lets the next call send just the new messages; without one, it needs the retained history.
8
8
 
9
9
  FastLLM calls stream internally. The helpers here turn their parts into Responses SSE frames; they don't decide storage, ownership, or billing policy."""
10
10
 
@@ -133,7 +133,7 @@ def response_usage(usage):
133
133
  def _response_base(rid, body, previous_response_id=None, created_at=None):
134
134
  result = dict(id=rid, object='response', created_at=int(created_at or time.time()), instructions=body.get('instructions'), model=body['model'])
135
135
  result.update(parallel_tool_calls=body.get('parallel_tool_calls', True), previous_response_id=previous_response_id,
136
- reasoning=body.get('reasoning'), store=True, temperature=body.get('temperature'))
136
+ reasoning=body.get('reasoning'), store=True, prompt_cache_key=body.get('prompt_cache_key'), temperature=body.get('temperature'))
137
137
  result.update(text=body.get('text', {'format':{'type':'text'}}), tool_choice=body.get('tool_choice', 'auto'),
138
138
  tools=body.get('tools', []), top_p=body.get('top_p'), metadata=body.get('metadata', {}))
139
139
  return result
@@ -152,18 +152,19 @@ def response_object(rid, body, comp, previous_response_id=None, created_at=None)
152
152
  # %% ../nbs/06a_responses.ipynb #73a2fc16
153
153
  class ResponseState(BasicRepr):
154
154
  "Canonical history and optional provider continuation metadata."
155
- def __init__(self, id, model, history, provider_response_id=None): store_attr()
155
+ def __init__(self, id, model, history, provider_response_id=None, prompt_cache_key=None): store_attr()
156
156
 
157
157
  # %% ../nbs/06a_responses.ipynb #b28fa30d
158
158
  @patch
159
159
  def to_dict(self:ResponseState):
160
160
  "This state as a JSON-ready dict"
161
- return dict(id=self.id, model=self.model, history=[msg2dict(m) for m in self.history], provider_response_id=self.provider_response_id)
161
+ return dict(id=self.id, model=self.model, history=[msg2dict(m) for m in self.history],
162
+ provider_response_id=self.provider_response_id, prompt_cache_key=self.prompt_cache_key)
162
163
 
163
164
  @patch(cls_method=True)
164
165
  def from_dict(cls:ResponseState, d):
165
166
  "The state `to_dict` produced `d` from"
166
- return cls(d['id'], d['model'], tuple(dict2msg(m) for m in d['history']), d['provider_response_id'])
167
+ return cls(d['id'], d['model'], tuple(dict2msg(m) for m in d['history']), d['provider_response_id'], d['prompt_cache_key'])
167
168
 
168
169
  # %% ../nbs/06a_responses.ipynb #74e09d18
169
170
  class ResponseStore:
@@ -221,6 +222,8 @@ class AsyncResponses:
221
222
  if isinstance(p, ToolResult) and not p.name: p.name = names.get(p.id, '')
222
223
  instructions = '\n\n'.join(filter(None, [body.get('instructions', ''), input_system]))
223
224
  provider_messages = new_msgs if previous and previous.provider_response_id else list(history)
225
+ key = body.get('prompt_cache_key') or (previous.prompt_cache_key if previous else None) or secrets.token_hex(16)
226
+ body = body | dict(prompt_cache_key=key)
224
227
  return ResponseTurn(_new_id('resp'), int(time.time()), body, history, provider_messages, instructions, previous,
225
228
  {f.__name__: f for f in server_tools or []}, kwargs)
226
229
 
@@ -233,7 +236,7 @@ async def call(self:AsyncResponses, turn):
233
236
  reasoning = body.get('reasoning') or {}
234
237
  tools = list(body.get('tools') or []) + [dict(type='function', **get_schema(f, pname='parameters')) for f in turn.server_tools.values()]
235
238
  kwargs = dict(previous_response_id=turn.provider_previous_id, system=turn.system or None, tools=tools or None)
236
- kwargs.update(tool_choice=body.get('tool_choice'), parallel_tool_calls=body.get('parallel_tool_calls', True),
239
+ kwargs.update(prompt_cache_key=body['prompt_cache_key'], tool_choice=body.get('tool_choice'), parallel_tool_calls=body.get('parallel_tool_calls', True),
237
240
  reasoning_effort=reasoning.get('effort'), max_tokens=body.get('max_output_tokens'), temperature=body.get('temperature'),
238
241
  cache_idxs=body.get('cache_idxs'), ttl=body.get('ttl'), web_search_options=body.get('web_search_options'))
239
242
  stream = await acomplete(turn.provider_messages, model=body['model'], stream=True, **kwargs, **turn.call_kwargs)
@@ -250,7 +253,7 @@ def state(self:AsyncResponses, turn, comp, results=()):
250
253
  "Build the next continuation state; `results` are server tool results answering `comp`'s calls."
251
254
  normalize_call_ids(comp)
252
255
  history = (*turn.history, comp.message, *([Msg('tool', list(results))] if results else []))
253
- return ResponseState(turn.id, turn.body['model'], history, comp.response_id)
256
+ return ResponseState(turn.id, turn.body['model'], history, comp.response_id, turn.body['prompt_cache_key'])
254
257
 
255
258
 
256
259
  # %% ../nbs/06a_responses.ipynb #7615b274
fastllm/types.py CHANGED
@@ -4,12 +4,12 @@
4
4
 
5
5
  # %% auto #0
6
6
  __all__ = ['FinishReason', 'api_registry', 'model_prices_url', 'sample_img_url', 'sample_doms', 'haik45', 'sonn45', 'sonn46',
7
- 'sonn', 'sonn5', 'opus46', 'opus48', 'opus', 'opus5', 'fable', 'fable5', 'fable51', 'gpt54', 'gpt54m',
8
- 'gpt55', 'codex54', 'codex54m', 'codex55', 'codex53spark', 'model_info_registry', 'modern_llm',
9
- 'effort_codes', 'mimo_v25_common', 'codex_pricing', 'sol', 'terra', 'luna', 'gpt56s', 'astra', 'Usage',
10
- 'APIRegistry', 'mk_completion', 'fn_schema', 'payload_kwargs', 'provider_req', 'get_api_key', 'wrap_typed',
11
- 'unwrap_typed', 'resize_b64', 'model_prices_meta', 'infer_api_name', 'get_model_meta', 'register_model_info',
12
- 'get_model_info', 'effort_code', 'effort_levels', 'resolve_effort', 'get_model_pricing', 'approx_pricing',
7
+ 'sonn', 'sonn5', 'opus46', 'opus48', 'opus', 'opus5', 'fable', 'fable5', 'fable51', 'gpt55', 'codex55',
8
+ 'codex53spark', 'model_info_registry', 'modern_llm', 'effort_codes', 'mimo_v25_common', 'codex_pricing',
9
+ 'sol', 'terra', 'luna', 'gpt56s', 'astra', 'Usage', 'APIRegistry', 'mk_completion', 'fn_schema',
10
+ 'payload_kwargs', 'provider_req', 'get_api_key', 'wrap_typed', 'unwrap_typed', 'resize_b64',
11
+ 'model_prices_meta', 'infer_api_name', 'get_model_meta', 'register_model_info', 'get_model_info',
12
+ 'effort_code', 'effort_levels', 'resolve_effort', 'get_model_pricing', 'approx_pricing',
13
13
  'is_deepseek_peak_hour', 'price_tier', 'tier_rate']
14
14
 
15
15
  # %% ../nbs/00_types.ipynb #b4d047fd
@@ -104,7 +104,8 @@ def fn_schema(t):
104
104
 
105
105
  # %% ../nbs/00_types.ipynb #28c698fe
106
106
  def payload_kwargs(msgs, model, stream=False, system=None, max_tokens=None, temperature=None, tools=None, tool_choice=None,
107
- parallel_tool_calls=None, previous_response_id=None, reasoning_effort=None, web_search_options=None, cache_idxs=None, ttl=None, stop_callables=None): pass
107
+ parallel_tool_calls=None, previous_response_id=None, prompt_cache_key=None, reasoning_effort=None, web_search_options=None,
108
+ cache_idxs=None, ttl=None, stop_callables=None): pass
108
109
 
109
110
  # %% ../nbs/00_types.ipynb #0c582256
110
111
  async def provider_req(
@@ -113,15 +114,16 @@ async def provider_req(
113
114
  body, # JSON request body
114
115
  params=None, # Query parameters
115
116
  stream=False, # Return an async generator of SSE events instead of the decoded response?
117
+ headers=None, # Extra headers for this request
116
118
  ):
117
119
  "POST `body` to `path` on `cli`: the decoded response, or SSE events when `stream`"
118
120
  url = cli._url(path)
119
121
  if not stream:
120
- try: return dict2obj(await cli.transport.request('POST', url, params=params, json=body))
122
+ try: return dict2obj(await cli.transport.request('POST', url, params=params, json=body, headers=headers))
121
123
  except (httpx2.HTTPStatusError, httpx2.RequestError) as e: raise e.api_error() from e
122
124
  async def _events():
123
125
  try:
124
- async for ev in cli.transport.stream('POST', url, params=params, json=body): yield dict2obj(ev)
126
+ async for ev in cli.transport.stream('POST', url, params=params, json=body, headers=headers): yield dict2obj(ev)
125
127
  except (httpx2.HTTPStatusError, httpx2.RequestError) as e: raise e.api_error() from e
126
128
  return _events()
127
129
 
@@ -197,11 +199,7 @@ opus48 = "claude-opus-4-8"
197
199
  opus = opus5 = "claude-opus-5"
198
200
  fable = fable5 = 'claude-fable-5'
199
201
  fable51 = 'claude-fable-5-1'
200
- gpt54 = "gpt-5.4"
201
- gpt54m = "gpt-5.4-mini"
202
202
  gpt55 = "gpt-5.5"
203
- codex54 = "gpt-5.4"
204
- codex54m = "gpt-5.4-mini"
205
203
  codex55 = "gpt-5.5"
206
204
  codex53spark = "gpt-5.3-codex-spark"
207
205
 
@@ -230,7 +228,8 @@ modern_llm = dict(supports_function_calling=True, supports_tool_choice=True, sup
230
228
  register_model_info('gpt-5.4', vendor_name='openai', base='gpt-5.4', supports_web_search=True)
231
229
 
232
230
  register_model_info('accounts/fireworks/models/deepseek-v4p1-flash', vendor_name='fireworks_ai',
233
- base='accounts/fireworks/models/deepseek-v4-flash-0731', supports_vision=True, supports_image_input=True)
231
+ base='accounts/fireworks/models/deepseek-v4-flash-0731', supports_vision=True, supports_image_input=True,
232
+ input_cost_per_token=0.30e-6, cache_read_input_token_cost=0.006e-6, output_cost_per_token=1.20e-6)
234
233
  register_model_info('glm-5.3-flash', vendor_name='zai', base='glm-5.3-flash', reasoning_effort_levels=['low', 'high', 'max'])
235
234
  register_model_info('accounts/fireworks/models/glm-5p3-flash', vendor_name='fireworks_ai', base='glm-5.3-flash', base_vendor_name='zai')
236
235
 
@@ -299,7 +298,7 @@ def _flat_rates(vendor_name, model):
299
298
  info = model_info_registry[vendor_name, model]
300
299
  for k in [k for k in info if 'cost' in k and k not in codex_pricing and k != 'search_context_cost_per_query']: del info[k]
301
300
 
302
- for model in (codex54, codex54m, codex55):
301
+ for model in (codex55,):
303
302
  register_model_info(model, 'codex', base=model, base_vendor_name='chatgpt', supports_web_search=True, max_input_tokens=256000, **codex_pricing)
304
303
  _flat_rates('codex', model)
305
304
 
@@ -1,8 +1,8 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-fastllm
3
- Version: 0.0.61
3
+ Version: 0.0.63
4
4
  Author-email: Kerem Turgutlu <keremturgutlu@gmail.com>
5
- License: Apache-2.0
5
+ License-Expression: Apache-2.0
6
6
  Project-URL: Repository, https://github.com/AnswerDotAI/fastllm
7
7
  Keywords: nbdev
8
8
  Classifier: Programming Language :: Python :: 3
@@ -0,0 +1,18 @@
1
+ fastllm/__init__.py,sha256=_yt9UDgFClBMmhhKeVz45D1Jlvpid0dbSrPSy_2bVtI,131
2
+ fastllm/_modidx.py,sha256=iyQLdOYLOPp87AmgdFQhL7QUxXMKh-TooHd6swPFuMc,46958
3
+ fastllm/acomplete.py,sha256=GNKQDaaDOJXzuVHozdkxnhwYFf9wZ_89UWNt7VxHLCE,11071
4
+ fastllm/anthropic.py,sha256=N8gfoDftlH-WFVO3CED4OGTv5qDTDGBHzp4JevgOFss,19347
5
+ fastllm/chat.py,sha256=fetuhePvXph2RnDpeQfA5SrRxgeMTmGOJA8XGXihh3s,25366
6
+ fastllm/codex.py,sha256=r6auZ8egia83aGkWcF4132AzW4SsBvKeDwg_CxyXj9E,5808
7
+ fastllm/gemini.py,sha256=56C3NJt8WLwx_XUWU-AElyfz_V5Zp2hIGmmkFhjFWn0,17527
8
+ fastllm/model_prices.json,sha256=hu5XfKDhsGQylcV75uJ-i8IXjz7jDH7ifTqsS56FvAg,2367128
9
+ fastllm/openai_chat.py,sha256=SDa8g5F5F58Li2pFHd9tSLS45f-OnI0VWvhfkP16aWM,12501
10
+ fastllm/openai_responses.py,sha256=5WIPznQQm2DCeUo520Yqd2qjqql6EZ3UodA1gyXCiiE,15518
11
+ fastllm/responses.py,sha256=nzB6Q90LTmJjZeSoUk36pEQQtRNcxBHkQN6NG7RlM9Y,23569
12
+ fastllm/streaming.py,sha256=dQ7ZFc4JEtPanoKXizww91rmYXTODTisQJb1FlSfWMU,8765
13
+ fastllm/types.py,sha256=Q1c6ZmhF_fMqE8EdZWVE50oc3SdDvD4hMuzO8MaKX_4,19368
14
+ python_fastllm-0.0.63.dist-info/METADATA,sha256=0TjN3ymQK-UTkJ3mqlkdk-WZkOCEjwbcblurBZfWMyw,8950
15
+ python_fastllm-0.0.63.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
16
+ python_fastllm-0.0.63.dist-info/entry_points.txt,sha256=WkWd04F3xVYFP5bsexaQe9Se6uelwE5ULoTtHWreO0s,133
17
+ python_fastllm-0.0.63.dist-info/top_level.txt,sha256=F8qodL7nEGUHGmzzqfhNKCTIr1i0D6cvudOnm3z7o0Y,8
18
+ python_fastllm-0.0.63.dist-info/RECORD,,
@@ -1,18 +0,0 @@
1
- fastllm/__init__.py,sha256=AYKW_EYx80n3SGgwsHo7M-J6zr9komnSTd3er5PYZ8k,131
2
- fastllm/_modidx.py,sha256=iyQLdOYLOPp87AmgdFQhL7QUxXMKh-TooHd6swPFuMc,46958
3
- fastllm/acomplete.py,sha256=6qhl1RL9muhQ7ESOTq9LDS3huwUo1k2kUve_1cETruo,10881
4
- fastllm/anthropic.py,sha256=N8gfoDftlH-WFVO3CED4OGTv5qDTDGBHzp4JevgOFss,19347
5
- fastllm/chat.py,sha256=f92AOIpJipEi3GZPnTKinJvxMU2RLdu_qEKkAUjgtcU,25099
6
- fastllm/codex.py,sha256=r6auZ8egia83aGkWcF4132AzW4SsBvKeDwg_CxyXj9E,5808
7
- fastllm/gemini.py,sha256=56C3NJt8WLwx_XUWU-AElyfz_V5Zp2hIGmmkFhjFWn0,17527
8
- fastllm/model_prices.json,sha256=hu5XfKDhsGQylcV75uJ-i8IXjz7jDH7ifTqsS56FvAg,2367128
9
- fastllm/openai_chat.py,sha256=SDa8g5F5F58Li2pFHd9tSLS45f-OnI0VWvhfkP16aWM,12501
10
- fastllm/openai_responses.py,sha256=n0uzAULex61rXNWuzvxSBV9-qHpqSQfl-7KBNk_GO6w,15440
11
- fastllm/responses.py,sha256=k5D3YS8zgvUBpk_qADPtvVD9CUOWl4iej_0qvi-GuVs,23166
12
- fastllm/streaming.py,sha256=dQ7ZFc4JEtPanoKXizww91rmYXTODTisQJb1FlSfWMU,8765
13
- fastllm/types.py,sha256=RrThFMYm9mARUVbyJLiOrPkSxJtJqIe1VcvmUYzYSsY,19301
14
- python_fastllm-0.0.61.dist-info/METADATA,sha256=0RAXbczKkO9yehLg4q67C5ZKBxIWLKO5aSHGIvxBdJI,8939
15
- python_fastllm-0.0.61.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
16
- python_fastllm-0.0.61.dist-info/entry_points.txt,sha256=WkWd04F3xVYFP5bsexaQe9Se6uelwE5ULoTtHWreO0s,133
17
- python_fastllm-0.0.61.dist-info/top_level.txt,sha256=F8qodL7nEGUHGmzzqfhNKCTIr1i0D6cvudOnm3z7o0Y,8
18
- python_fastllm-0.0.61.dist-info/RECORD,,