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

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