letta-nightly 0.6.16.dev20250128104041__py3-none-any.whl → 0.6.17.dev20250129174639__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.

Potentially problematic release.


This version of letta-nightly might be problematic. Click here for more details.

Files changed (35) hide show
  1. letta/__init__.py +1 -1
  2. letta/agent.py +0 -3
  3. letta/client/client.py +5 -5
  4. letta/client/streaming.py +29 -20
  5. letta/constants.py +1 -1
  6. letta/functions/function_sets/multi_agent.py +55 -49
  7. letta/functions/functions.py +0 -1
  8. letta/functions/helpers.py +149 -9
  9. letta/llm_api/llm_api_tools.py +20 -12
  10. letta/llm_api/openai.py +15 -13
  11. letta/orm/agent.py +14 -2
  12. letta/orm/job.py +1 -1
  13. letta/orm/sqlalchemy_base.py +12 -4
  14. letta/schemas/job.py +17 -1
  15. letta/schemas/letta_request.py +2 -7
  16. letta/schemas/llm_config.py +9 -0
  17. letta/schemas/message.py +51 -22
  18. letta/schemas/openai/chat_completion_response.py +2 -2
  19. letta/schemas/run.py +1 -2
  20. letta/server/rest_api/app.py +5 -1
  21. letta/server/rest_api/chat_completions_interface.py +256 -0
  22. letta/server/rest_api/optimistic_json_parser.py +185 -0
  23. letta/server/rest_api/routers/openai/chat_completions/__init__.py +0 -0
  24. letta/server/rest_api/routers/openai/chat_completions/chat_completions.py +161 -0
  25. letta/server/rest_api/routers/v1/agents.py +22 -32
  26. letta/server/server.py +12 -12
  27. letta/services/job_manager.py +7 -12
  28. letta/services/tool_manager.py +17 -1
  29. letta/system.py +20 -0
  30. letta/utils.py +24 -1
  31. {letta_nightly-0.6.16.dev20250128104041.dist-info → letta_nightly-0.6.17.dev20250129174639.dist-info}/METADATA +4 -4
  32. {letta_nightly-0.6.16.dev20250128104041.dist-info → letta_nightly-0.6.17.dev20250129174639.dist-info}/RECORD +35 -31
  33. {letta_nightly-0.6.16.dev20250128104041.dist-info → letta_nightly-0.6.17.dev20250129174639.dist-info}/LICENSE +0 -0
  34. {letta_nightly-0.6.16.dev20250128104041.dist-info → letta_nightly-0.6.17.dev20250129174639.dist-info}/WHEEL +0 -0
  35. {letta_nightly-0.6.16.dev20250128104041.dist-info → letta_nightly-0.6.17.dev20250129174639.dist-info}/entry_points.txt +0 -0
letta/llm_api/openai.py CHANGED
@@ -1,8 +1,8 @@
1
1
  import warnings
2
- from typing import Generator, List, Optional, Union
2
+ from typing import AsyncGenerator, List, Optional, Union
3
3
 
4
4
  import requests
5
- from openai import OpenAI
5
+ from openai import AsyncOpenAI
6
6
 
7
7
  from letta.llm_api.helpers import add_inner_thoughts_to_functions, convert_to_structured_output, make_post_request
8
8
  from letta.local_llm.constants import INNER_THOUGHTS_KWARG, INNER_THOUGHTS_KWARG_DESCRIPTION, INNER_THOUGHTS_KWARG_DESCRIPTION_GO_FIRST
@@ -158,7 +158,7 @@ def build_openai_chat_completions_request(
158
158
  return data
159
159
 
160
160
 
161
- def openai_chat_completions_process_stream(
161
+ async def openai_chat_completions_process_stream(
162
162
  url: str,
163
163
  api_key: str,
164
164
  chat_completion_request: ChatCompletionRequest,
@@ -229,9 +229,10 @@ def openai_chat_completions_process_stream(
229
229
  stream_interface.stream_start()
230
230
 
231
231
  n_chunks = 0 # approx == n_tokens
232
+ chunk_idx = 0
232
233
  try:
233
- for chunk_idx, chat_completion_chunk in enumerate(
234
- openai_chat_completions_request_stream(url=url, api_key=api_key, chat_completion_request=chat_completion_request)
234
+ async for chat_completion_chunk in openai_chat_completions_request_stream(
235
+ url=url, api_key=api_key, chat_completion_request=chat_completion_request
235
236
  ):
236
237
  assert isinstance(chat_completion_chunk, ChatCompletionChunkResponse), type(chat_completion_chunk)
237
238
 
@@ -348,6 +349,7 @@ def openai_chat_completions_process_stream(
348
349
 
349
350
  # increment chunk counter
350
351
  n_chunks += 1
352
+ chunk_idx += 1
351
353
 
352
354
  except Exception as e:
353
355
  if stream_interface:
@@ -380,24 +382,24 @@ def openai_chat_completions_process_stream(
380
382
  return chat_completion_response
381
383
 
382
384
 
383
- def openai_chat_completions_request_stream(
385
+ async def openai_chat_completions_request_stream(
384
386
  url: str,
385
387
  api_key: str,
386
388
  chat_completion_request: ChatCompletionRequest,
387
- ) -> Generator[ChatCompletionChunkResponse, None, None]:
389
+ ) -> AsyncGenerator[ChatCompletionChunkResponse, None]:
388
390
  data = prepare_openai_payload(chat_completion_request)
389
391
  data["stream"] = True
390
- client = OpenAI(
392
+ client = AsyncOpenAI(
391
393
  api_key=api_key,
392
394
  base_url=url,
393
395
  )
394
- stream = client.chat.completions.create(**data)
395
- for chunk in stream:
396
+ stream = await client.chat.completions.create(**data)
397
+ async for chunk in stream:
396
398
  # TODO: Use the native OpenAI objects here?
397
399
  yield ChatCompletionChunkResponse(**chunk.model_dump(exclude_none=True))
398
400
 
399
401
 
400
- def openai_chat_completions_request(
402
+ async def openai_chat_completions_request(
401
403
  url: str,
402
404
  api_key: str,
403
405
  chat_completion_request: ChatCompletionRequest,
@@ -410,8 +412,8 @@ def openai_chat_completions_request(
410
412
  https://platform.openai.com/docs/guides/text-generation?lang=curl
411
413
  """
412
414
  data = prepare_openai_payload(chat_completion_request)
413
- client = OpenAI(api_key=api_key, base_url=url)
414
- chat_completion = client.chat.completions.create(**data)
415
+ client = AsyncOpenAI(api_key=api_key, base_url=url)
416
+ chat_completion = await client.chat.completions.create(**data)
415
417
  return ChatCompletionResponse(**chat_completion.model_dump())
416
418
 
417
419
 
letta/orm/agent.py CHANGED
@@ -4,6 +4,7 @@ from typing import TYPE_CHECKING, List, Optional
4
4
  from sqlalchemy import JSON, Index, String
5
5
  from sqlalchemy.orm import Mapped, mapped_column, relationship
6
6
 
7
+ from letta.constants import MULTI_AGENT_TOOLS
7
8
  from letta.orm.block import Block
8
9
  from letta.orm.custom_columns import EmbeddingConfigColumn, LLMConfigColumn, ToolRulesColumn
9
10
  from letta.orm.message import Message
@@ -15,7 +16,7 @@ from letta.schemas.agent import AgentType
15
16
  from letta.schemas.embedding_config import EmbeddingConfig
16
17
  from letta.schemas.llm_config import LLMConfig
17
18
  from letta.schemas.memory import Memory
18
- from letta.schemas.tool_rule import ToolRule
19
+ from letta.schemas.tool_rule import TerminalToolRule, ToolRule
19
20
 
20
21
  if TYPE_CHECKING:
21
22
  from letta.orm.agents_tags import AgentsTags
@@ -114,6 +115,16 @@ class Agent(SqlalchemyBase, OrganizationMixin):
114
115
 
115
116
  def to_pydantic(self) -> PydanticAgentState:
116
117
  """converts to the basic pydantic model counterpart"""
118
+ # add default rule for having send_message be a terminal tool
119
+ tool_rules = self.tool_rules
120
+ if not tool_rules:
121
+ tool_rules = [
122
+ TerminalToolRule(tool_name="send_message"),
123
+ ]
124
+
125
+ for tool_name in MULTI_AGENT_TOOLS:
126
+ tool_rules.append(TerminalToolRule(tool_name=tool_name))
127
+
117
128
  state = {
118
129
  "id": self.id,
119
130
  "organization_id": self.organization_id,
@@ -123,7 +134,7 @@ class Agent(SqlalchemyBase, OrganizationMixin):
123
134
  "tools": self.tools,
124
135
  "sources": [source.to_pydantic() for source in self.sources],
125
136
  "tags": [t.tag for t in self.tags],
126
- "tool_rules": self.tool_rules,
137
+ "tool_rules": tool_rules,
127
138
  "system": self.system,
128
139
  "agent_type": self.agent_type,
129
140
  "llm_config": self.llm_config,
@@ -136,4 +147,5 @@ class Agent(SqlalchemyBase, OrganizationMixin):
136
147
  "updated_at": self.updated_at,
137
148
  "tool_exec_environment_variables": self.tool_exec_environment_variables,
138
149
  }
150
+
139
151
  return self.__pydantic_model__(**state)
letta/orm/job.py CHANGED
@@ -9,7 +9,7 @@ from letta.orm.mixins import UserMixin
9
9
  from letta.orm.sqlalchemy_base import SqlalchemyBase
10
10
  from letta.schemas.enums import JobStatus
11
11
  from letta.schemas.job import Job as PydanticJob
12
- from letta.schemas.letta_request import LettaRequestConfig
12
+ from letta.schemas.job import LettaRequestConfig
13
13
 
14
14
  if TYPE_CHECKING:
15
15
  from letta.orm.job_messages import JobMessage
@@ -1,6 +1,7 @@
1
1
  from datetime import datetime
2
2
  from enum import Enum
3
3
  from functools import wraps
4
+ from pprint import pformat
4
5
  from typing import TYPE_CHECKING, List, Literal, Optional, Tuple, Union
5
6
 
6
7
  from sqlalchemy import String, and_, func, or_, select
@@ -504,7 +505,14 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base):
504
505
  model.metadata = self.metadata_
505
506
  return model
506
507
 
507
- def to_record(self) -> "BaseModel":
508
- """Deprecated accessor for to_pydantic"""
509
- logger.warning("to_record is deprecated, use to_pydantic instead.")
510
- return self.to_pydantic()
508
+ def pretty_print_columns(self) -> str:
509
+ """
510
+ Pretty prints all columns of the current SQLAlchemy object along with their values.
511
+ """
512
+ if not hasattr(self, "__table__") or not hasattr(self.__table__, "columns"):
513
+ raise NotImplementedError("This object does not have a '__table__.columns' attribute.")
514
+
515
+ # Iterate over the columns correctly
516
+ column_data = {column.name: getattr(self, column.name, None) for column in self.__table__.columns}
517
+
518
+ return pformat(column_data, indent=4, sort_dicts=True)
letta/schemas/job.py CHANGED
@@ -1,8 +1,9 @@
1
1
  from datetime import datetime
2
2
  from typing import Optional
3
3
 
4
- from pydantic import Field
4
+ from pydantic import BaseModel, Field
5
5
 
6
+ from letta.constants import DEFAULT_MESSAGE_TOOL, DEFAULT_MESSAGE_TOOL_KWARG
6
7
  from letta.orm.enums import JobType
7
8
  from letta.schemas.enums import JobStatus
8
9
  from letta.schemas.letta_base import OrmMetadataBase
@@ -38,3 +39,18 @@ class JobUpdate(JobBase):
38
39
 
39
40
  class Config:
40
41
  extra = "ignore" # Ignores extra fields
42
+
43
+
44
+ class LettaRequestConfig(BaseModel):
45
+ use_assistant_message: bool = Field(
46
+ default=True,
47
+ description="Whether the server should parse specific tool call arguments (default `send_message`) as `AssistantMessage` objects.",
48
+ )
49
+ assistant_message_tool_name: str = Field(
50
+ default=DEFAULT_MESSAGE_TOOL,
51
+ description="The name of the designated message tool.",
52
+ )
53
+ assistant_message_tool_kwarg: str = Field(
54
+ default=DEFAULT_MESSAGE_TOOL_KWARG,
55
+ description="The name of the message argument in the designated message tool.",
56
+ )
@@ -6,8 +6,8 @@ from letta.constants import DEFAULT_MESSAGE_TOOL, DEFAULT_MESSAGE_TOOL_KWARG
6
6
  from letta.schemas.message import MessageCreate
7
7
 
8
8
 
9
- class LettaRequestConfig(BaseModel):
10
- # Flags to support the use of AssistantMessage message types
9
+ class LettaRequest(BaseModel):
10
+ messages: List[MessageCreate] = Field(..., description="The messages to be sent to the agent.")
11
11
  use_assistant_message: bool = Field(
12
12
  default=True,
13
13
  description="Whether the server should parse specific tool call arguments (default `send_message`) as `AssistantMessage` objects.",
@@ -22,11 +22,6 @@ class LettaRequestConfig(BaseModel):
22
22
  )
23
23
 
24
24
 
25
- class LettaRequest(BaseModel):
26
- messages: List[MessageCreate] = Field(..., description="The messages to be sent to the agent.")
27
- config: LettaRequestConfig = Field(default=LettaRequestConfig(), description="Configuration options for the LettaRequest.")
28
-
29
-
30
25
  class LettaStreamingRequest(LettaRequest):
31
26
  stream_tokens: bool = Field(
32
27
  default=False,
@@ -88,6 +88,7 @@ class LLMConfig(BaseModel):
88
88
  model_endpoint="https://api.openai.com/v1",
89
89
  model_wrapper=None,
90
90
  context_window=8192,
91
+ put_inner_thoughts_in_kwargs=True,
91
92
  )
92
93
  elif model_name == "gpt-4o-mini":
93
94
  return cls(
@@ -97,6 +98,14 @@ class LLMConfig(BaseModel):
97
98
  model_wrapper=None,
98
99
  context_window=128000,
99
100
  )
101
+ elif model_name == "gpt-4o":
102
+ return cls(
103
+ model="gpt-4o",
104
+ model_endpoint_type="openai",
105
+ model_endpoint="https://api.openai.com/v1",
106
+ model_wrapper=None,
107
+ context_window=128000,
108
+ )
100
109
  elif model_name == "letta":
101
110
  return cls(
102
111
  model="memgpt-openai",
letta/schemas/message.py CHANGED
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  import copy
2
4
  import json
3
5
  import warnings
@@ -25,6 +27,7 @@ from letta.schemas.letta_message import (
25
27
  ToolReturnMessage,
26
28
  UserMessage,
27
29
  )
30
+ from letta.system import unpack_message
28
31
  from letta.utils import get_utc_time, is_utc_datetime, json_dumps
29
32
 
30
33
 
@@ -176,9 +179,47 @@ class Message(BaseMessage):
176
179
  json_message["created_at"] = self.created_at.isoformat()
177
180
  return json_message
178
181
 
182
+ @staticmethod
183
+ def to_letta_messages_from_list(
184
+ messages: List[Message],
185
+ use_assistant_message: bool = True,
186
+ assistant_message_tool_name: str = DEFAULT_MESSAGE_TOOL,
187
+ assistant_message_tool_kwarg: str = DEFAULT_MESSAGE_TOOL_KWARG,
188
+ ) -> List[LettaMessage]:
189
+ if use_assistant_message:
190
+ message_ids_to_remove = []
191
+ assistant_messages_by_tool_call = {
192
+ tool_call.id: msg
193
+ for msg in messages
194
+ if msg.role == MessageRole.assistant and msg.tool_calls
195
+ for tool_call in msg.tool_calls
196
+ }
197
+ for message in messages:
198
+ if (
199
+ message.role == MessageRole.tool
200
+ and message.tool_call_id in assistant_messages_by_tool_call
201
+ and assistant_messages_by_tool_call[message.tool_call_id].tool_calls
202
+ and assistant_message_tool_name
203
+ in [tool_call.function.name for tool_call in assistant_messages_by_tool_call[message.tool_call_id].tool_calls]
204
+ ):
205
+ message_ids_to_remove.append(message.id)
206
+
207
+ messages = [msg for msg in messages if msg.id not in message_ids_to_remove]
208
+
209
+ # Convert messages to LettaMessages
210
+ return [
211
+ msg
212
+ for m in messages
213
+ for msg in m.to_letta_message(
214
+ use_assistant_message=use_assistant_message,
215
+ assistant_message_tool_name=assistant_message_tool_name,
216
+ assistant_message_tool_kwarg=assistant_message_tool_kwarg,
217
+ )
218
+ ]
219
+
179
220
  def to_letta_message(
180
221
  self,
181
- assistant_message: bool = False,
222
+ use_assistant_message: bool = False,
182
223
  assistant_message_tool_name: str = DEFAULT_MESSAGE_TOOL,
183
224
  assistant_message_tool_kwarg: str = DEFAULT_MESSAGE_TOOL_KWARG,
184
225
  ) -> List[LettaMessage]:
@@ -201,7 +242,7 @@ class Message(BaseMessage):
201
242
  for tool_call in self.tool_calls:
202
243
  # If we're supporting using assistant message,
203
244
  # then we want to treat certain function calls as a special case
204
- if assistant_message and tool_call.function.name == assistant_message_tool_name:
245
+ if use_assistant_message and tool_call.function.name == assistant_message_tool_name:
205
246
  # We need to unpack the actual message contents from the function call
206
247
  try:
207
248
  func_args = json.loads(tool_call.function.arguments)
@@ -264,11 +305,12 @@ class Message(BaseMessage):
264
305
  elif self.role == MessageRole.user:
265
306
  # This is type UserMessage
266
307
  assert self.text is not None, self
308
+ message_str = unpack_message(self.text)
267
309
  messages.append(
268
310
  UserMessage(
269
311
  id=self.id,
270
312
  date=self.created_at,
271
- content=self.text,
313
+ content=message_str or self.text,
272
314
  )
273
315
  )
274
316
  elif self.role == MessageRole.system:
@@ -311,26 +353,13 @@ class Message(BaseMessage):
311
353
  assert "tool_call_id" in openai_message_dict, openai_message_dict
312
354
 
313
355
  # Convert from 'function' response to a 'tool' response
314
- # NOTE: this does not conventionally include a tool_call_id, it's on the caster to provide it
315
- message_args = dict(
316
- user_id=user_id,
317
- agent_id=agent_id,
318
- model=model,
319
- # standard fields expected in an OpenAI ChatCompletion message object
320
- role=MessageRole.tool, # NOTE
321
- text=openai_message_dict["content"],
322
- name=openai_message_dict["name"] if "name" in openai_message_dict else None,
323
- tool_calls=openai_message_dict["tool_calls"] if "tool_calls" in openai_message_dict else None,
324
- tool_call_id=openai_message_dict["tool_call_id"] if "tool_call_id" in openai_message_dict else None,
325
- created_at=created_at,
326
- )
327
356
  if id is not None:
328
357
  return Message(
329
358
  agent_id=agent_id,
330
359
  model=model,
331
360
  # standard fields expected in an OpenAI ChatCompletion message object
332
361
  role=MessageRole.tool, # NOTE
333
- content=[TextContent(text=openai_message_dict["content"])],
362
+ content=[TextContent(text=openai_message_dict["content"])] if openai_message_dict["content"] else [],
334
363
  name=openai_message_dict["name"] if "name" in openai_message_dict else None,
335
364
  tool_calls=openai_message_dict["tool_calls"] if "tool_calls" in openai_message_dict else None,
336
365
  tool_call_id=openai_message_dict["tool_call_id"] if "tool_call_id" in openai_message_dict else None,
@@ -343,7 +372,7 @@ class Message(BaseMessage):
343
372
  model=model,
344
373
  # standard fields expected in an OpenAI ChatCompletion message object
345
374
  role=MessageRole.tool, # NOTE
346
- content=[TextContent(text=openai_message_dict["content"])],
375
+ content=[TextContent(text=openai_message_dict["content"])] if openai_message_dict["content"] else [],
347
376
  name=openai_message_dict["name"] if "name" in openai_message_dict else None,
348
377
  tool_calls=openai_message_dict["tool_calls"] if "tool_calls" in openai_message_dict else None,
349
378
  tool_call_id=openai_message_dict["tool_call_id"] if "tool_call_id" in openai_message_dict else None,
@@ -375,7 +404,7 @@ class Message(BaseMessage):
375
404
  model=model,
376
405
  # standard fields expected in an OpenAI ChatCompletion message object
377
406
  role=MessageRole(openai_message_dict["role"]),
378
- content=[TextContent(text=openai_message_dict["content"])],
407
+ content=[TextContent(text=openai_message_dict["content"])] if openai_message_dict["content"] else [],
379
408
  name=openai_message_dict["name"] if "name" in openai_message_dict else None,
380
409
  tool_calls=tool_calls,
381
410
  tool_call_id=None, # NOTE: None, since this field is only non-null for role=='tool'
@@ -388,7 +417,7 @@ class Message(BaseMessage):
388
417
  model=model,
389
418
  # standard fields expected in an OpenAI ChatCompletion message object
390
419
  role=MessageRole(openai_message_dict["role"]),
391
- content=[TextContent(text=openai_message_dict["content"])],
420
+ content=[TextContent(text=openai_message_dict["content"])] if openai_message_dict["content"] else [],
392
421
  name=openai_message_dict["name"] if "name" in openai_message_dict else None,
393
422
  tool_calls=tool_calls,
394
423
  tool_call_id=None, # NOTE: None, since this field is only non-null for role=='tool'
@@ -420,7 +449,7 @@ class Message(BaseMessage):
420
449
  model=model,
421
450
  # standard fields expected in an OpenAI ChatCompletion message object
422
451
  role=MessageRole(openai_message_dict["role"]),
423
- content=[TextContent(text=openai_message_dict["content"])],
452
+ content=[TextContent(text=openai_message_dict["content"])] if openai_message_dict["content"] else [],
424
453
  name=openai_message_dict["name"] if "name" in openai_message_dict else None,
425
454
  tool_calls=tool_calls,
426
455
  tool_call_id=openai_message_dict["tool_call_id"] if "tool_call_id" in openai_message_dict else None,
@@ -433,7 +462,7 @@ class Message(BaseMessage):
433
462
  model=model,
434
463
  # standard fields expected in an OpenAI ChatCompletion message object
435
464
  role=MessageRole(openai_message_dict["role"]),
436
- content=[TextContent(text=openai_message_dict["content"] or "")],
465
+ content=[TextContent(text=openai_message_dict["content"])] if openai_message_dict["content"] else [],
437
466
  name=openai_message_dict["name"] if "name" in openai_message_dict else None,
438
467
  tool_calls=tool_calls,
439
468
  tool_call_id=openai_message_dict["tool_call_id"] if "tool_call_id" in openai_message_dict else None,
@@ -116,7 +116,7 @@ class MessageDelta(BaseModel):
116
116
 
117
117
  content: Optional[str] = None
118
118
  tool_calls: Optional[List[ToolCallDelta]] = None
119
- # role: Optional[str] = None
119
+ role: Optional[str] = None
120
120
  function_call: Optional[FunctionCallDelta] = None # Deprecated
121
121
 
122
122
 
@@ -132,7 +132,7 @@ class ChatCompletionChunkResponse(BaseModel):
132
132
 
133
133
  id: str
134
134
  choices: List[ChunkChoice]
135
- created: datetime.datetime
135
+ created: Union[datetime.datetime, str]
136
136
  model: str
137
137
  # system_fingerprint: str # docs say this is mandatory, but in reality API returns None
138
138
  system_fingerprint: Optional[str] = None
letta/schemas/run.py CHANGED
@@ -3,8 +3,7 @@ from typing import Optional
3
3
  from pydantic import Field
4
4
 
5
5
  from letta.orm.enums import JobType
6
- from letta.schemas.job import Job, JobBase
7
- from letta.schemas.letta_request import LettaRequestConfig
6
+ from letta.schemas.job import Job, JobBase, LettaRequestConfig
8
7
 
9
8
 
10
9
  class RunBase(JobBase):
@@ -12,7 +12,7 @@ from starlette.middleware.base import BaseHTTPMiddleware
12
12
  from starlette.middleware.cors import CORSMiddleware
13
13
 
14
14
  from letta.__init__ import __version__
15
- from letta.constants import ADMIN_PREFIX, API_PREFIX
15
+ from letta.constants import ADMIN_PREFIX, API_PREFIX, OPENAI_API_PREFIX
16
16
  from letta.errors import BedrockPermissionError, LettaAgentNotFoundError, LettaUserNotFoundError
17
17
  from letta.log import get_logger
18
18
  from letta.orm.errors import DatabaseTimeoutError, ForeignKeyConstraintViolationError, NoResultFound, UniqueConstraintViolationError
@@ -22,6 +22,7 @@ from letta.server.constants import REST_DEFAULT_PORT
22
22
  # NOTE(charles): these are extra routes that are not part of v1 but we still need to mount to pass tests
23
23
  from letta.server.rest_api.auth.index import setup_auth_router # TODO: probably remove right?
24
24
  from letta.server.rest_api.interface import StreamingServerInterface
25
+ from letta.server.rest_api.routers.openai.chat_completions.chat_completions import router as openai_chat_completions_router
25
26
 
26
27
  # from letta.orm.utilities import get_db_session # TODO(ethan) reenable once we merge ORM
27
28
  from letta.server.rest_api.routers.v1 import ROUTERS as v1_routes
@@ -241,6 +242,9 @@ def create_application() -> "FastAPI":
241
242
  app.include_router(users_router, prefix=ADMIN_PREFIX)
242
243
  app.include_router(organizations_router, prefix=ADMIN_PREFIX)
243
244
 
245
+ # openai
246
+ app.include_router(openai_chat_completions_router, prefix=OPENAI_API_PREFIX)
247
+
244
248
  # /api/auth endpoints
245
249
  app.include_router(setup_auth_router(server, interface, password), prefix=API_PREFIX)
246
250