July 26, 2026
promptfoo

Notebook

Fixing a Promptfoo result collision bug with duplicate provider ids

I fixed a Promptfoo bug where evaluation results collided and were displayed incorrectly when multiple providers used the same provider id.

promptfooLLMEvaluation
日本語

Guide

Contents

  1. Background
  2. What Was Happening
  3. The Fix
  4. Maintainer Follow-Up
  5. Summary

I fixed a bug I found while using Promptfoo, so this note summarizes what happened.

The PR is here.
promptfoo PR #10208

Background

I was evaluating whether a normal text response or a JSON response would produce better output, using a configuration like this.

prompts:
  - |
    Answer in one short sentence.
    Topic: {{topic}}
 
providers:
  - id: openai:chat:gpt-5-mini
    label: "plain text"
    config:
      temperature: 0
      max_tokens: 24
 
  - id: openai:chat:gpt-5-mini
    label: "json format"
    config:
      temperature: 1
      max_tokens: 96
      response_format:
        type: json_schema
        json_schema:
          name: custom_name
          strict: true
          schema:
            type: object
            additionalProperties: false
            required:
              - message
            properties:
              message:
                type: string
                description: message
 
tests:
  - vars:
      topic: why reproducible eval results matter
    assert:
      - type: contains-any
        value:
          - eval
          - reproducible
          - consistency
 
  - vars:
      topic: why model configuration should be visible in comparison views
    assert:
      - type: contains-any
        value:
          - configuration
          - comparison
          - visible

When I checked the results, I expected to see one column for the text output pattern and another for the JSON output pattern. Instead, only one side appeared because the results collided.

Evaluation results with the same provider id colliding into one column

In LLM evaluation, it is common to compare the same model with different settings. For example, you might compare different temperature values. Promptfoo's documentation also describes this use case.
Promptfoo evaluate-llm-temperature

There was a workaround: adding label to each provider made the results display correctly.

providers:
  - id: openai:chat:gpt-5-mini
    label: "plain text"
    config:
      max_tokens: 128
 
  - id: openai:chat:gpt-5-mini
    label: "json format"
    config:
      max_tokens: 128
      response_format:
      # response_format details omitted

Evaluation results separated correctly by provider labels

Still, it took me a while to notice the workaround. I felt Promptfoo should either warn when the same provider id is used more than once, or display the results correctly without requiring a label.

That is what this PR fixed.

What Was Happening

Promptfoo assigns a prompt index to each provider/prompt combination so it can store evaluation results in a table.

For example, consider a case where two providers return the same provider id, and there is only one prompt.

Provider orderProvider idPromptExpected column index
Firstduplicate-providerTest prompt0
Secondduplicate-providerTest prompt1

Even when the provider id and prompt are the same, there are two providers, so the evaluation table needs two result columns.

However, the previous implementation used a key like this to find which prompt/provider column a result should be written to.

`${provider}:${promptId}` -> index

In this case, both providers returned the same provider id, and the prompt was also the same, so the Map key became identical.

Provider orderMap keyStored index
Firstduplicate-provider:<promptId>0
Secondduplicate-provider:<promptId>1

JavaScript Map cannot hold multiple values for the same key, so the second value overwrote the first one.

As a result, the Map looked like this.

Map keyindex
duplicate-provider:<promptId>1

The relevant code was evaluator.ts#L2044.

Because the required index = 0 information was lost, the first provider's evaluation result was also associated with the second column.

The Fix

After the fix, the Map stores an array of indices for the same key instead of a single index. commit 4043eca

`${provider}:${promptId}` -> [index1, index2, ...]

In the example above, the Map becomes:

Map keyindices
duplicate-provider:<promptId>[0, 1]

This allows Promptfoo to keep every corresponding column index even when the same provider id and prompt combination appears more than once.

However, changing the value to an array is not enough on its own. Promptfoo still needs to know whether the provider currently being evaluated should use [0, 1][0] or [0, 1][1].

To solve that, the implementation counts how many times the same provider key has appeared during evaluation.

Provider orderProvider idOccurrence indexPrompt index to use
Firstduplicate-provider0[0, 1][0] = 0
Secondduplicate-provider1[0, 1][1] = 1

In code, the occurrence count is managed with a Map, and the prompt index matching that occurrence index is used.

const promptIndices = promptIndexMap.get(`${providerKey}:${promptId}`);
const promptIdx = promptIndices?.[providerOccurrenceIndex];

This made it possible to store each evaluation result in a separate column, even when the same provider id and prompt combination appeared multiple times.

Maintainer Follow-Up

I opened the PR with the fix above, and Michael from the Promptfoo maintainer team added a further improvement.

My initial fix kept the providerKey:promptId key and changed the value from a single index to an array of indices.

`${provider}:${promptId}` -> [index1, index2, ...]

That follow-up went one step further: it stopped looking up result columns from providerKey:promptId altogether.

Instead, when prompt columns are created, the implementation now keeps the columns owned by each provider directly.

Conceptually, the structure looks like this.

Provider orderProviderColumns
FirstfirstProvider[{ promptIdx: 0, prompt: Test prompt }]
SecondsecondProvider[{ promptIdx: 1, prompt: Test prompt }]

In other words, rather than finding columns later with a key, each provider receives its own promptIdx at the time the columns are created.

type ProviderColumns = {
  provider: ApiProvider;
  columns: { promptIdx: number; prompt: Prompt }[];
};

With this shape, evaluation can simply process columnsByProvider in order.

for (const { provider, columns } of columnsByProvider) {
  for (const { promptIdx, prompt } of columns) {
    // Store this provider/prompt result in the promptIdx column.
  }
}

The important point is that the execution plan now receives the actual columns that were created, instead of reconstructing them from display or identifier strings such as provider id or prompt id.

This makes the behavior more robust for cases like these.

CaseWhy it can cause troubleHow the maintainer fix handles it
Multiple providers have the same provider idproviderKey collidesEach provider has its own columns
Different providers have the same labelUsing label as the provider key can collideColumns are kept per provider object
Different prompts resolve to the same prompt idPrompt ids derived from labels can collideColumns are kept when each prompt column is created
Tests filter providers or promptsColumn indices can shift after filteringThe already-created promptIdx is reused

The resume metrics path was also updated.

When metrics are restored from existing evaluation results, duplicate providers can resolve to the same stored prompt. If multiple columns share the same metrics object, updating one result can affect the other.

To avoid that, existing metrics are cloned with structuredClone, so each column gets independent metrics.

metrics: existingPrompt?.metrics
  ? structuredClone(existingPrompt.metrics)
  : createDefaultPromptMetrics()

My first fix avoided the collision by allowing multiple indices for the same key. Michael's follow-up went further and changed the design so result columns are no longer reconstructed from string keys that can collide.

Summary

The results are now displayed correctly.

After the fix, evaluation results with the same provider id are shown in separate columns

This is separate from the main topic of this article, but while working on this fix, I also found and fixed an error in the documentation. promptfoo PR #10209

Promptfoo is very useful for prompt evaluation, so I have been using it quite a lot recently.
This was a small fix, but I am glad I was able to contribute in some way.