Where can I see all the threads I've created with the API?

1、Where can I see all the threads I’ve created with the API?
2、After I created a thread, I created the session with that thread id, why does it show that there is no such id?
openai.NotFoundError: Error code: 404 - {‘error’: {‘message’: “No thread found with id ‘run_zdE51vuY4FlBpEy2dYb3870E’.”, ‘type’: ‘invalid_request_error’, ‘param’: None, ‘code’: None}}

  1. Currently, there is no way to list all threads. You need to manage this yourself, keeping the thread ids somewhere. I read that threads can exist up to 60 days. (see below)

  2. It seems you are trying to get thread using run id run_zdE51vuY4FlBpEy2dYb3870E, thread id format is thread_abc123...

Ok thanks. I seem to be confusing run_id with thread_id

You can find all the Threads on Organisation page. It’s Hidden by default, and you can update the visibility

Then you can go to https://platform.openai.com/threads to see all the previous threads.

4 Likes

So there is a list , but you can’t manage old threads or delete them… anyway thanks for the tip on how to find them, very useful.

How can I get all threads I have created using the API?

1 Like

Currently there is no native way of retrieving all the threads… as in there is no client.beta.threads.list() function .

However with clever use of metadata with the betaassi framework, it is possible to keep track of threads created. See here for details (openairetro/examples/listablethreads at main · icdev2dev/openairetro · GitHub)

# these primitives are exposed
ListableThread.create(**kwargs)
ListableThread.delete(thread_id)
ListableThread.list()

Because this uses metadata to maintain that list, it is limited by the amount of bytes in the metadata that can be used.

Can anyone clarify why is this obvious API endpoint missing?

3 Likes

@jeremiah-OpenAI please!! :smiley:

Thank you for your help. I’ve been unable to find the ‘thread’ button on the left side of the OpenAI interface.

As a participant in an organizational account, how can I ask the account owner or administrator to unlock this feature? Is there a URL link or a guide available?

Thanks

Hey OpenAI … you need to speak up and give us a good answer on this one. We just don’t get why you haven’t (yet?) provided Python .threads.list() and .theads.delete() support. We need this. At this point I have, probably, 100s of thread out there holding data I want to delete. If you can’t provide Python .threads.list() and .theads.delete() support, please explain why not and if later an estimate, even guestimate, or when.

from openai import OpenAI
client = OpenAI()

thread_list = [
    "thread_dyqFxhfNT8LQR2LUeBbjzfRq",
]
for thread in thread_list:
    response = client.beta.threads.delete(thread)
    print(response)

Denying thread listings is likely to prevent all types of unlimited chat snooping by organization members.

Thank you. Could you explain what this code means? I have permissions to open threads with my personal account, and I can see the thread button on the left dashboard of my personal account. Clicking on it allows me to view the content of previous assistant threads under my account. It feels quite unsettling that all these are remembered by OpenAI. With your code, response = client.beta.threads.delete(thread) is it possible to delete the content of a specific thread ID like the assistant ID on the OpenAI website?

However, this deletion only removes the record from the webpage, making it invisible to users and the owners of organizational accounts, but the content of the thread is still retained in the official database.

Thank you

That is a documented method to “delete a thread”: delete the whole thread of messages by ID.

https://platform.openai.com/docs/api-reference/threads/deleteThread

You must know the thread ID. You can check for success return value 200 of the API call. Did the delete work, making the contents irretrievable? You can attempt to retrieve a message listing and message ID from the thread before and after the delete key and see the difference.

Your organization has a setting to turn off the web playground listing of threads for organization members that are invited as “readers” or also for organization “owner”. The internal endpoint for retrieving the listing then returns an error.

OpenAI retains API data for up to 30 days for safety and moderation concerns.

Thank you for your response. However, how would I go about deleting a thread ID using Python code? The official cookbook code you provided is quite unique:

curl https://api.openai.com/v1/threads/thread_abc123 \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $OPENAI_API_KEY" \
  -H "OpenAI-Beta: assistants=v2" \
  -X DELETE
  1. If my code below can respond to a list of questions through the assistant sequentially, how should I delete the thread after all the questions have been answered?
  2. Additionally, you mentioned that OpenAI retains API data for up to 30 days for safety and moderation concerns. However, I see that my personal account’s assistant thread records still have data from almost six months ago.
    How can I ensure that I delete these messages uploaded through the assistant?

Thank you.

Here is the segment of my Python code related to managing threads:

segments = [Q1, Q2, Q3, ...] 
# Load or create a conversation thread
try:
    with open('thread_id_test.txt', 'r') as f:
        thread_id = f.readline().strip()
    print("Using existing thread ID:", thread_id)
except FileNotFoundError:
    thread = client.beta.threads.create()
    thread_id = thread.id
    with open('thread_id_test.txt', 'w') as f:
        f.write(thread_id)
    print("Created new thread ID:", thread_id)

# Initially send a system prompt to the conversation thread
client.beta.threads.messages.create(
    thread_id=thread_id,
    role="user",
    content=system_prompt
)

# Loop through each text segment
countm = 0
for segment in segments:
    countm += 1
    # Send messages to the conversation thread
    client.beta.threads.messages.create(
        thread_id=thread_id,
        role="user",
        content=segment  # Send current question
    )

    # Execute the assistant for the current segment
    run = client.beta.threads.runs.create(
        thread_id=thread_id,
        assistant_id=assistant_id
    )

    # Check the execution results
    while True:
        run = client.beta.threads.runs.retrieve(
            thread_id=thread_id,
            run_id=run.id
        )
        if run.status == "completed":
            print(f"Run completed. Segment {countm}")
            break
        elif run.status == "failed":
            print("Run failed with error:", run.last_error)
            break
        time.sleep(2)

    messages = client.beta.threads.messages.list(
        thread_id=thread_id
    )
    message = messages.data[0].content[0].text.value
    print(message)

Thank you for your response. I have some other questions above. Thank you.

Scroll up just a few more replies. I pasted Python delete.

You of course must track the IDs of threads you create in API.

Thank you for your reply. Later, I updated it to this version using today’s updated gpt4o to avoid the risk of inputting an ID that might not exist or has already been deleted. Unfortunately, gpt4o still incorrectly used the function openai.Thread.delete(thread_id). It was your code that corrected it.

Additionally, I have a question:

After clearing the thread ID, does it only disappear from the web page at https://platform.openai.com/threads, but still exist in OpenAI’s official database?

Best regards;

Is it just an ostrich method, simply hiding what you asked the assistant from other people in the organization?

def delete_threads(thread_ids):
    for thread_id in thread_ids:
        try:
            # Attempt to delete the specified thread
            response = client.beta.threads.delete(thread_id)
            print(f"Thread {thread_id} deleted successfully.")
        except Exception as e:
            # Handle other errors
            print(f"An error occurred while deleting thread {thread_id}: {e}")

I’m curious what you mean and what the symptom is of “still exist in OpenAI’s official database” to you. Does it matter if a thread database simply has a flag “deleted” during the retention period if they can never be recovered by API once that delete operation is performed?

If you want to ensure thread operations work correctly, they have to be paired with an API key with organization and project that created them.

Those threads created with and that are visible in the Playground have no project association, they instead are made by web session backend.

Here’s a tool that I wrote up in response to this thread - to let you see all the threads you’ve created in the API across projects you are owner of or within Playground. The endpoint respects the project or organization option to not allow thread listing. It will need you to follow the instructions you see in code, and then write practical API methods instead of the example methods that give a few thread IDs just to show operation.

import base64
import requests
import os
import json
import time

oauth_instructions = """
Your platform.openai.com oAuth token must be obtained after web login.
Use browser's developer tools -> network inspection (reload page).
Find post request to https://api.openai.com/dashboard/onboarding/login
Copy the long header string after `Authorization: Bearer`,
set as OPENAI_OAUTH env variable in your OS or environment.
"""
token = os.environ.get('OPENAI_OAUTH')
session = None
apikey = os.environ.get('OPENAI_API_KEY')
key = apikey[:7] + "..." + apikey[-4:]
orgs = [{"id":"", "title": "playground", "projects": {"data": []}}]
orgs =[]
headers = {
"Host": "api.openai.com",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:125.0) Gecko/20100101 Firefox/125.0",
"Accept": "*/*",
"Accept-Language": "en-US,en;q=0.5",
"Accept-Encoding": "gzip, deflate, br",
"Referer": "https://platform.openai.com/",
"OpenAI-Beta": "assistants=v2",
"Origin": "https://platform.openai.com",
"Connection": "keep-alive",
"Sec-Fetch-Dest": "empty",
"Sec-Fetch-Mode": "cors",
"Sec-Fetch-Site": "same-site",
"Pragma": "no-cache",
"Cache-Control": "no-cache",
"TE": "trailers",
#"Content-Type": "application/json",
}

def get_token(mytoken):
    try:
        oheader, payload, osignature = mytoken.split('.')
        # print(f"trying token {oheader[:10]}.{payload[:10]}...")
        opayload = json.loads(base64.urlsafe_b64decode(payload + '=='))
        oauth_email = opayload['https://api.openai.com/profile']['email']
        oauth_expires = opayload["exp"]
        oauth_session_id = opayload["session_id"]
    except Exception as e:
        print(f"Couldn't parse the provided oauth token!! ERROR:\n{e}{oauth_instructions}")
        raise
    if time.time() > oauth_expires:
        raise ValueError("oAuth {oheader[:10]}.{payload[:10]} expired. "
                         "get a new one.{oauth_instructions}")
    else:
        print(f"{oauth_email} token {oheader[:10]}.{payload[:10]}...")
        print(f"(oAuth expires in {(oauth_expires-time.time())/60/60/24:.1f} days.)")
    return token

login_response=None
def login(auth):
    global headers, session, orgs, login_response
    headers.update({"Authorization": f"Bearer {get_token(auth)}"})
    url = "https://api.openai.com/dashboard/onboarding/login"
    try:
        cors = requests.options(url, headers=headers)
        cors_headers = json.loads(json.dumps(dict(cors.headers)))
        login_result = requests.post(url, headers=headers, json={}) # application/json
        login_response = login_result.json()
        # print(json.dumps(login_response, indent=2))  # huge dump
    except Exception as e:
        print(f"API login request failed! {e}")
        raise
    try:
        session = login_response["user"]["session"]["sensitive_id"]
        print(f"session key: {session}")
    except Exception as e:
        raise ValueError("Error: session secret could not be parsed from login response.")
    headers.update({"Authorization": f"Bearer {session}"})
    print(f"login OK: {login_response['user']['name']} ({login_response['user']['phone_number']})")
    for data in login_response['user']['orgs']['data']:
        orgs.append(data)
        #orgs.update({data["id"]: data["title"]})
        
    #print(f"organizations: {orgs}")
    return session, orgs

def menu_input(prompt: str, maximum_value: int, allow_0: bool = True, 
               exit_option: int = None, other_allowed: list = []) -> int:
    """
    Prompts the user to select an option from a menu, validating the input.

    Args:
    - prompt (str): The initial prompt to display to the user.
    - maximum_value (int): The maximum valid integer value that can be input.
    - allow_0 (bool): Whether '0' is a valid input (default True).
    - exit_option (int or None): An optional integer value that, if entered, immediately returns as a valid option.
                                 Set to None to disable the exit option.
    - other_allowed (list): Additional integer values considered valid even if not in the normal range.

    Returns:
    - int: The user's validated choice.
    """

    # Create a detailed prompt showing all valid options
    valid_range = f"1 to {maximum_value}"
    if allow_0:
        valid_range = "0, " + valid_range
    if other_allowed:
        valid_range += ", " + ", ".join(map(str, other_allowed))
    if exit_option is not None:  # Only show exit option if it's explicitly provided
        valid_range += f", or {exit_option} (exit option)"

    full_prompt = f"-- {prompt}\n-- Valid inputs: {valid_range}; Your choice: "

    while True:
        user_input = input(full_prompt)
        try:
            selection = int(user_input)
            if exit_option is not None and selection == exit_option:
                return selection
            if allow_0 and selection == 0:
                return selection
            if selection in other_allowed:
                return selection
            if 1 <= selection <= maximum_value:
                return selection
            else:
                print("Please enter a valid option.")
        except ValueError:
            print("Invalid input. Please enter an integer.")



def org_chooser(myorgs):
    print("\n-- Choose an organization and project matching API key used")
    orgmap = [{"title": "Playground", "org": ""}]
    for o in myorgs:
        orgmap.append({"title": o["title"], "org": o["id"], "ptitle": None, "proj": None})
        for p in o["projects"]["data"]:
            orgmap.append({"title": o["title"], "org": p["organization_id"], 
                           "ptitle": p["title"], "proj": p["id"]})

    for i, o in enumerate(orgmap):
        pr = f"[{i}] {o['title']}"
        if o["org"]:
            pr += f": {o['org']}"
            if o["ptitle"]:
                pr += f":\n\tProject: {o['ptitle']} - {o['proj']}"
        print(pr)

    # User input for selection
    selection = menu_input("Select an organization by index",
                           maximum_value = len(orgmap) - 1)
    selected_org = orgmap[selection]

    headers = {}
    if selected_org["org"]:  # Only add header if 'org' is not empty
        headers["OpenAI-Organization"] = selected_org["org"]
    if "proj" in selected_org and selected_org["proj"]:
        headers["OpenAI-Project"] = selected_org["proj"]
    return headers


if __name__ == "__main__":
    login(token)
    while True:
        headers["OpenAI-Organization"] = None
        headers["OpenAI-Project"] = None
        org_headers = org_chooser(orgs)
        headers.update(org_headers)
        url = "https://api.openai.com/v1/threads?limit=4"
        response = requests.get(url, headers=headers,)
        if response.status_code != 200:
            print(f"HTTP error {response.status_code}: {response.text}")
        else:
            print("OMG Threads\n" + json.dumps(response.json()['data'], indent=3))
            print("=== end of threads, try another org choice for this demo ===")

(if you can’t improve this code for a utility implementation, you also shouldn’t be sending API requests with possible catastrophic data loss…)

2 Likes

Thank you for your detailed response.

  1. Symptom of “still exist in OpenAI’s official database”: I’m just curious why the assistant’s input and output results in my personal thread are retained for so long. OpenAI states that for security reasons, data is retained for 30 days, but I see complete records from over four months ago. I wonder if the content is used for OpenAI’s training.
  2. As mentioned, I hope that the data I input will not be used for official training. However, the advantage of threads is that even after closing the assistant, I can refer back to previous QA prompts.
  3. I see records in the thread for both the Playground and when using the API to call the assistant.

But I have issues with identity switching. Previously, I created user API keys, not the new project API keys. There is a “Default organization setting” below where I can select Organization A or Individual B. Could you explain the dependency between the two?

  • If I create a new API key based on the Default organization, will that key belong to the organization or the individual?
  • Or do all API keys on that page change according to the Default organization setting, and thus all become either organizational or individual?
  1. Finally, how do I run the code you provided? If I want to see the thread list for an organizational account instead of an individual account, can I do that, or is it only for individual information?

What should be filled in for these two variables? Whose API key should be used? What is OPENAI_OAUTH?

You mentioned that it’s best not to delete too many thread IDs’ data, as the prompts designed earlier might contain valuable insights! It’s just that we have produced so many threads that it takes time to find them. Even my boss doesn’t want to unlock permissions for all members.

token = os.environ.get('OPENAI_OAUTH')
session = None
apikey = os.environ.get('OPENAI_API_KEY')

Thank you.