Skip to content

pi: gpt-5/mini/nano/5-5-pro always 400 — reasoning.effort "none" sent because thinkingLevelMap is unset #286

Description

@dgokeeffe

Summary

ucode pi with databricks-openai/system.ai.gpt-5, -gpt-5-mini, -gpt-5-nano or -gpt-5-5-pro fails on every request with:

OpenAI API error (400): 400 status code (no body)

Cause: _pi_gpt_model_entry declares reasoning: True but never sets thinkingLevelMap, so when thinking is off Pi sends reasoning: {effort: "none"}, which those four models reject.

Root cause

src/ucode/agents/pi.py:157-172:

def _pi_gpt_model_entry(model_id: str) -> dict:
    entry: dict = {"id": model_id, "contextWindow": ..., "maxTokens": ...}
    if "gpt-5" in model_id.lower().replace(".", "-"):
        entry["reasoning"] = True
        entry["input"] = ["text", "image"]
    return entry

Pi's contract (pi-ai/dist/api/openai-responses.js:243-247) is that the model entry declares its own off-state; absent thinkingLevelMap it defaults to "none":

else if (model.provider !== "github-copilot" && model.thinkingLevelMap?.off !== null) {
    params.reasoning = { effort: (model.thinkingLevelMap?.off ?? "none") };
}

"none" is only valid on gpt-5.1+. We never set the override, so older ids get an invalid value.

Measured, against /ai-gateway/codex/v1/responses

Model effort:"none" effort:"minimal" omitted
gpt-5, gpt-5-mini, gpt-5-nano 400 200 200
gpt-5-5-pro 400 400 200
gpt-5-1/-2/-3-codex/-4/-4-mini/-4-nano/-5/-6-luna/-6-sol/-6-terra 200 200 200

Gateway response on failure:

{"error_code":"BAD_REQUEST","message":"{\n  \"error\": {\n    \"message\": \"Unsupported value: 'none' is not supported with the 'gpt-5' model. Supported values are: 'minimal', 'low', 'medium', and 'high'.\"..."}

I bisected the rest of Pi's Responses payload against the gateway and it is all fine: store:false, prompt_cache_key, prompt_cache_retention:"24h", prompt_cache_options, include:["reasoning.encrypted_content"], developer role, flat tool schemas with additionalProperties:false, and the underscore session_id / x-client-request-id affinity headers — all 200.

Why the error message is useless

Pi's error-body.js recovery only reads error.error, and the gateway wraps its payload as {"error_code", "message"}. So e.error === undefined, the body is discarded, and every gateway 400 renders identically. Filed upstream separately. That is why this took so long to pin down, but the config bug is ours.

Proposed fix

Set the off-state explicitly in _pi_gpt_model_entry. thinkingLevelMap: {"off": None} makes Pi omit reasoning entirely (the ?.off !== null branch above) — verified 200 on all 14 ids, and it's the same pattern we already use for the Gemini 3.x entries.

if "gpt-5" in model_id.lower().replace(".", "-"):
    entry["reasoning"] = True
    entry["input"] = ["text", "image"]
    entry["thinkingLevelMap"] = {"off": None}

{"off": "minimal"} is the alternative if we want to keep an explicit low-reasoning signal, but it still 400s on gpt-5-5-pro, so it would need per-model handling.

Worth noting databricks-openai is the only provider block in pi.py with no compat/capability hardening — databricks-claude needed supportsEagerToolInputStreaming: False and databricks-mlflow needed supportsStore/supportsStrictMode: False. Same class of omission. The docstrings at pi.py:12-22 and the note at databricks.py:2446 ("required for oss only") should be updated too.

Happy to put up the PR.

Environment

  • ucode 0.1.0, pi 0.83.0
  • workspace: dbc-a5d4177a-49dc.cloud.databricks.com
  • config: ~/.ucode/pi-home/.pi/agent/models.json

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions