OpenRouter Pydantic Code sample

This method lets you call the OpenRouter API, pass in a specific Pydantic schema, and get a response which should conform to the schema that you sent.

def extract_autism_info(custom_input_text=None, custom_input_name=None, custom_model_name=None,
                        custom_system_prompt=None, custom_reasoning_effort=None, custom_force_override=None,
                        vaers_id=None, custom_experiment=None):
    """
    Extract autism information from VAERS report text.
    
    Args:
        custom_input_text (str, optional): Custom input text to use instead of predefined inputs
        custom_input_name (str, optional): Custom name for the input (used in filename)
        custom_model_name (str, optional): Custom model name to use
        custom_system_prompt (str, optional): Custom system prompt to use
        custom_reasoning_effort (str, optional): Custom reasoning effort level
        custom_force_override (bool, optional): Custom force override setting
        vaers_id (str, optional): VAERS ID for the report (used in filename, defaults to 0)
    
    Returns:
        dict: The processed response data
    """
    # Use custom values if provided, otherwise use defaults
    model_name = custom_model_name or MODEL_NAME
    system_prompt = custom_system_prompt or SystemPrompt.spas2
    reasoning_effort = custom_reasoning_effort or REASONING_EFFORT
    force_override = custom_force_override if custom_force_override is not None else True  # Default to True

    # Handle VAERS ID
    vaers_id_str = str(vaers_id) if vaers_id is not None else "0"

    experiment = custom_experiment or EXPERIMENT_NAME
    model_name_str = model_name.replace('/', '-').replace(':', '-')

    # Handle input text
    if custom_input_text:
        input_info = custom_input_text
        input_name = custom_input_name or "custom_input"
    else:
        # Use default input
        selected_input = Inputs.ias190064
        input_info = selected_input.value
        input_name = selected_input.name

    formatted_input = format_input(input_info)

    # Construct filename and directory structure
    model_name_slug = slugify.slugify(model_name_str)
    filename = f'autism_info_{model_name_slug}_{vaers_id_str}.json'
    # Create subfolder based on model name to prevent too many files in one folder
    model_folder = f'json/{experiment}/{model_name_slug}'
    filepath = f'{model_folder}/{filename}'

    # Always try to load existing file first
    existing_data = None
    if os.path.exists(filepath):
        print(f"Found existing file: {filepath}")
        try:
            with open(filepath, 'r') as f:
                existing_data = json.load(f)
            
            # Check if the file already contains valid JSON
            if existing_data.get('contains_valid_json', False):
                print("File contains valid JSON. Skipping API call.")
                return existing_data
            else:
                print("File exists but does not contain valid JSON. Making API call.")
        except Exception as e:
            print(f"Error loading existing file: {e}")
            print("Making API call.")
            existing_data = None
    else:
        print(f"No existing file found. Making API call.")

    if not force_override and existing_data and existing_data.get('contains_valid_json', False):
        print("Using existing data with valid JSON without API call.")
        return existing_data

    # If we reach here, we need to make an API call
    json_schema_text = json.dumps(VAERSReport.model_json_schema(), indent=2)
    prompt = f"""
    {system_prompt} 
    
    Report: {formatted_input}
    
    JSON Schema:
    {json_schema_text}
    """

    before = time.time()

    # Initialize response variables
    api_error = None

    try:
        url = "https://openrouter.ai/api/v1/chat/completions"

        headers = {
            "Authorization": f"Bearer {api_key}",
            "Content-Type": "application/json",
            "HTTP-Referer": "https://botflo.com/", 
            "X-Title": "LLM Accuracy Comparisons for Structured Outputs",
        }

        payload = {
            "model": model_name,
            "messages": [
                {"role": "user", "content": f'{prompt}'}
            ],
            "reasoning": {
                "effort": "high"
            }

        }

        response_full = requests.post(url, headers=headers, data=json.dumps(payload))
        response = response_full.json()
        after = time.time()
        elapsed = after - before
        inner_response_text = response['choices'][0]['message']['content']
        full_response_json = response_full.json()
        now = datetime.datetime.now()
        timestamp = now.strftime('%Y_%m_%d_%H_%M_%S')
        log_file = f'{model_name_str}_{vaers_id_str}_{timestamp}.json'
        with open(f'json/logs/{log_file}', 'w+') as f:
            json.dump(response, f, indent=2)
    except Exception as e:
        after = time.time()
        elapsed = after - before
        api_error = str(e)
        inner_response_text = f"API Error: {api_error}"
        full_response_json = json.dumps({"error": api_error, "timestamp": time.time()})

    # No JSON processing here - just save the raw response

    # Extract token usage from full_response_json
    prompt_tokens = None
    completion_tokens = None
    if not api_error and full_response_json:
        try:
            # full_response_json is already a dict, not a string
            if isinstance(full_response_json, dict) and 'usage' in full_response_json:
                usage = full_response_json['usage']
                prompt_tokens = usage.get('prompt_tokens')
                completion_tokens = usage.get('completion_tokens')
            elif isinstance(full_response_json, str):
                # Handle case where it might be a JSON string
                full_response_data = json.loads(full_response_json)
                if 'usage' in full_response_data:
                    usage = full_response_data['usage']
                    prompt_tokens = usage.get('prompt_tokens')
                    completion_tokens = usage.get('completion_tokens')
        except Exception as e:
            print(f"Error extracting token usage: {e}")

    print('Before new item assignment')
    try:
        new_item = {
            "model_full_name": get_model_full_name(model_name),
            "input_text": formatted_input,
            "system_prompt": system_prompt,
            'json_schema_text': json_schema_text,
            "inner_response_text": inner_response_text,
            "full_response_json": full_response_json,
            "time_elapsed": elapsed,
            "api_error": api_error,
            "prompt_tokens": prompt_tokens,
            "completion_tokens": completion_tokens
        }
    except Exception as e:
        print(f"Error creating new_item: {e}")
        # In case of exception, create a minimal item with basic fields
        new_item = {
            "model_full_name": "unknown",
            "input_text": formatted_input,
            "system_prompt": system_prompt,
            'json_schema_text': json_schema_text,
            "inner_response_text": inner_response_text,
            "full_response_json": full_response_json,
            "time_elapsed": elapsed,
            "api_error": str(e),
            "prompt_tokens": prompt_tokens,
            "completion_tokens": completion_tokens
        }

    print('After new item assignment')

    # Create experiment directory and model subfolder if they don't exist
    os.makedirs(f'json/{experiment}', exist_ok=True)
    os.makedirs(model_folder, exist_ok=True)
    with open(filepath, 'w+') as f:
        json.dump(new_item, f, indent=2)

    return new_item

There is a lot more going on in this code than what is the absolute minimum necessary, so let me focus on the main part

This is the prompt which is sent to the API

json_schema_text = json.dumps(VAERSReport.model_json_schema(), indent=2)
prompt = f"""
{system_prompt} 

Report: {formatted_input}

JSON Schema:
{json_schema_text}
"""

There are three parts to this prompt:

The system instruction

The formatted input

The JSON schema in text format

You construct the prompt by concatenating all these three elements and send it to the OpenRouter API

url = "https://openrouter.ai/api/v1/chat/completions"

headers = {
    "Authorization": f"Bearer {api_key}",
    "Content-Type": "application/json",
    "HTTP-Referer": "https://botflo.com/",  # Optional. Site URL for rankings on openrouter.ai.
    "X-Title": "LLM Accuracy Comparisons for Structured Outputs",
}

payload = {
    "model": model_name,
    "messages": [
        {"role": "user", "content": f'{prompt}'}
    ],
    "reasoning": {
        "effort": "high"
    }

}

response_full = requests.post(url, headers=headers, data=json.dumps(payload))
response = response_full.json()

As I have written before I prefer to directly call the OpenRouter API rather than using the Python SDK

One of the most important things you need to know about LLMs when you are trying to extract structured output:

There are many ways for this LLM response to fail

  • A simple API error (usually LLM provider is down, or some system issue on OpenRouter’s side). This is very rare
  • The response text does not contain pure JSON (even though that is what we request in the prompt)
  • The response text does not contain valid JSON even as a substring
  • There is valid JSON, but it does not conform to the schema we went to the LLM

Handing these errors elegantly (for example, in some cases a simple retry is sufficient to fix the problem) so that you can still get the results that you are expecting is worthy of its own course!