Skip to content

A run can execute a model the persisted flow does not name — POST /api/v2/workflows does not build what the database holds #1372

Description

@rafaelgiln

Found while validating #1334 (PR #1369). Neither the database nor the widget predicted what the run executed, so no observable the suite currently reads would have caught it.

Symptom

Measured 2 in 12 full-file runs of openai-compatible-provider-setup.spec.ts on 1.12.0.dev19. The Playground run fails with:

Error code: 404 - {'error': {'message': 'This is not a chat model and thus not supported in the
v1/chat/completions endpoint. Did you mean to use v1/completions?', 'type': 'invalid_request_error',
'param': 'model'}}

gpt-4o-mini is a chat model. The only ids this provider offers that produce that error are its completions-only ones — babbage-002 / davinci-002, the first entries of its default-enabled set — which is the signature of an empty model field falling back to the provider's first default-enabled id.

Why "the selection was reverted" is refuted, not assumed

The obvious reading is the known editor race: a debounced PATCH /api/v1/flows/{id} lands after the pick and carries the pre-selection state. That was tested and does not explain this.

The spec re-reads GET /api/v1/flows/{id} at the last moment the run can still be influenced — after the Playground modal is open, immediately before button-send. On both occurrences that read returned:

{ models: ["gpt-4o-mini"], providers: ["OpenAI Compatible"], credential: "" }

The widget (value-dropdown-model_model) read gpt-4o-mini too. So at send time the database and the UI agreed, and the run still executed something else: POST /api/v2/workflows did not build what the database holds. A repair loop was written, measured against this, and removed for that reason — re-selecting cannot fix a state that is already correct.

The reversion race is real and separate: it was observed once in 5 in the same session, with the node left on the editor's mount default (claude-opus-5 / Anthropic). That one the persisted-binding poll catches. This one it does not.

What is already in place

PR #1369 left the pre-send re-read in the spec as attribution, not repair, so the next occurrence says so in one line instead of implying a stale selection. The failure message is:

the persisted binding must still be this provider's model when the run is sent — if the run then executes another model, the run did not build the persisted flow

That is the extent of what #1334 claimed. The mechanism is not diagnosed.

Investigation directive

Product first, and instrument the boundary. (a) Capture the POST /api/v2/workflows request body on a failing run and compare the model field it carries against GET /api/v1/flows/{id} read at the same instant — that single comparison decides whether the frontend sends a stale in-memory graph or the backend resolves the field differently. (b) If the request is correct, follow the backend: instantiation.py reads model.value[0], and an empty or unresolvable value is what produces the first-default fallback — establish which. (c) Establish the blast radius: this is the unified ModelInput on the v2 run path, so it is not specific to OpenAI Compatible — the same fallback on a provider whose first default is a chat model would run silently and green, which is the more dangerous form and the reason this is worth chasing.

Why it matters more than its rate suggests

Here it fails loudly only because this provider's first default-enabled id happens to be completions-only. A provider whose first default is a working chat model produces a passing run against a model nobody selected — the #1169 silent-substitution class. Any spec that pins a model and asserts on the reply is exposed.

Deliverables (Done when)

  • The request/response boundary is instrumented on a failing run, and the verdict names which side substitutes the model.
  • If the run does not build the persisted flow, that is recorded as a product finding with a filed upstream ticket, and this issue stays open until it lands in the nightly.
  • The blast radius is stated: which providers' first default-enabled id is a chat model, hence where this would pass silently.
  • Whatever observable does predict the executed model is identified, so specs can gate on it — the persisted binding demonstrably does not.

Metadata

Metadata

Assignees

Labels

area:model-providersProvedores de modelosfollow-upApproved exception: follow-up of merged work (ROADMAP Intake)

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions