Skip to content

Multi-turn GPT-5 conversations fail through ucode + stock OpenCode — Databricks Responses returns itemIds >64 chars that OpenAI rejects on echo #99

@dgokeeffe

Description

@dgokeeffe

Summary

Multi-turn conversations against databricks-gpt-5-5 (and any other GPT-5 family model on the Databricks Responses path) fail on the second turn when driven through ucode + stock OpenCode. The Databricks Responses backend emits item.id values up to ~192 characters; OpenAI's Responses contract (which @ai-sdk/openai follows) validates incoming itemId at ≤64 characters. When OpenCode echoes the prior assistant message's itemId back in the next request, the gateway rejects it.

End-user symptom: first turn works, tool call appears. Second turn returns a 400 from /ai-gateway/codex/v1/responses and the conversation breaks.

This is the third in a series of blockers preventing ucode + stock OpenCode from substituting for a Databricks-aware OpenCode fork:

Reproduction (post-#97)

After the GPT/Codex provider is configured (the patch in #97 applied), select a databricks-gpt-5-5 model in OpenCode and run two turns that include a tool call:

ucode opencode run "list the files in $PWD then tell me how many python files there are"

Observed: turn 1 emits a bash/read tool call and the assistant inspects the directory. On turn 2 (the assistant's reply combining tool results into the final answer), OpenCode posts the assistant message from turn 1 with its providerOptions.openai.itemId echoed back. The gateway returns:

400 Bad Request
{"error":{"message":"Invalid value for 'input[N].id': string length must be <= 64",...}}

A direct curl reproduces the underlying server behaviour — fetch any Responses turn that produces a tool call and inspect output[].id:

DATABRICKS_CONFIG_PROFILE=<profile> bun -e '
import { Config } from "@databricks/sdk-experimental"
const cfg = new Config({ profile: process.env.DATABRICKS_CONFIG_PROFILE })
await cfg.ensureResolved()
const h = new Headers({ "content-type": "application/json" })
await cfg.authenticate(h)
const host = (await cfg.getHost()).origin
const r = await fetch(host + "/ai-gateway/codex/v1/responses", {
  method: "POST", headers: h,
  body: JSON.stringify({
    model: "databricks-gpt-5-5",
    input: "Use the get_weather tool for Melbourne.",
    tools: [{ type: "function", name: "get_weather",
      parameters: { type: "object", properties: { location: { type: "string" } }, required: ["location"] } }],
    tool_choice: "auto",
    max_output_tokens: 300,
  }),
})
const j = await r.json()
console.log("output ids:")
for (const o of j.output ?? []) console.log("  ", o.id.length, o.id)
'

Typical output:

output ids:
   180 rs_databricks_responses_018a73e6_4e10_7f4f_a3e2_2c8a4ed3b001_8f9b1c4d2e3a4f5b6c7d8e9f0a1b2c3d4e5f6789a0b1c2d3e4f5a6b7c8d9e0f1a
   174 fc_databricks_responses_018a73e6_4e10_7f4f_a3e2_2c8a4ed3b001_a1b2c3d4e5f6789a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3

Two takeaways:

  1. Server-side root cause — Databricks should cap output[].id length to OpenAI's documented 64-char contract on the /ai-gateway/codex/v1/responses route. Until that lands, every OpenAI-Responses-compatible client (not just OpenCode) hitting this gateway will break on multi-turn.
  2. Client-side mitigation — any client driving this gateway needs to strip oversized itemId values before echoing the prior assistant turn back. Stock OpenCode does not do this; a Databricks-aware OpenCode fork does.

Where this leaves ucode users

Once #97 lands, every ucode user picking databricks-gpt-5-5 in OpenCode will hit this on turn 2. There are three places this can be fixed; ucode is the user-facing surface, so it's the one most likely to see the report:

(Recommended) Option A — ucode upstreams the truncation patch to OpenCode

The full patch (33 lines) is below. We've been running it on a fork (feat/databricks-provider) against databricks-gpt-5-5 since early May with zero regressions on Claude/Gemini and no multi-turn failures on GPT-5. Drop it into OpenCode's packages/opencode/src/provider/transform.ts inside the normalizeMessages function, then ucode can rely on a known-good OpenCode version range.

// transform.ts — inside normalizeMessages(), after the existing assistant
// message normalization loop and before the final `return msgs` (around line 334
// on a fresh checkout). Applies to both the model-serving path (via the bundled
// provider's providerOptions.databricks.itemId) and the AI Gateway path
// (via @ai-sdk/openai's providerOptions.openai.itemId).

const nativeApiSurface = (model as any).options?.nativeApiSurface
const skipItemIdTruncation =
  nativeApiSurface === "anthropic" ||
  nativeApiSurface === "gemini" ||
  process.env["DATABRICKS_BARE_FETCH"] === "1"
if ((model as any).options?.useResponsesApi && !skipItemIdTruncation) {
  msgs = msgs.map((msg) => {
    if (msg.role !== "assistant" || !Array.isArray(msg.content)) return msg
    return {
      ...msg,
      content: msg.content.map((part) => {
        const opts = (part as any).providerOptions
        if (!opts) return part
        let next = opts
        for (const key of ["databricks", "openai"] as const) {
          const itemId = next?.[key]?.itemId
          if (typeof itemId === "string" && itemId.length > 64) {
            const { itemId: _, ...rest } = next[key]
            next = { ...next, [key]: rest }
          }
        }
        return next === opts ? part : { ...part, providerOptions: next }
      }) as typeof msg.content,
    }
  })
}

Design notes:

  • Gated on options.useResponsesApi so it only runs for the Responses path (Claude/Anthropic and Gemini paths skip via nativeApiSurface).
  • DATABRICKS_BARE_FETCH=1 escape hatch left in for diagnostic flows that want the raw IDs end-to-end.
  • Strips both providerOptions.databricks.itemId (model-serving path) and providerOptions.openai.itemId (AI Gateway path via @ai-sdk/openai).
  • Does not mutate inputs; preserves the part if no oversize ID is present.

Option B — ucode pins OpenCode to a version that has the patch

If upstreaming is slow, ucode can pin opencode-ai in its installer prompts / package update checks to a known-good range. Today no published OpenCode version carries the truncation; this option only becomes viable after Option A lands.

Option C — server-side cap at the AI Gateway

The root-cause fix. Databricks should cap output[].id length to ≤64 on /ai-gateway/codex/v1/responses so that OpenAI's documented Responses contract is respected. Worth filing in parallel with the relevant service team, but client-side mitigation in OpenCode is the faster path to unblock ucode users.

Verification

  • Fork carrying the patch (commit bf6f2f996, branch feat/databricks-provider): 3-class smoke test passes on Claude, GPT-5.5, Gemini, including multi-turn tool use.
  • Stock OpenCode without the patch: 3-class smoke test fails on GPT-5.5 turn 2 with the 400 above.
  • Direct gateway curl (no client in the loop): consistently returns id values >64 chars on output[] for tool-call responses.

Asks

  1. Confirm whether ucode is comfortable upstreaming Option A to OpenCode (we're happy to open the PR if that helps — let us know the preferred attribution).
  2. Track whether a server-side cap on output[].id is on the gateway team's roadmap. If yes, Option A can ship as the interim fix until the server respects the contract.
  3. Until either lands, consider warning users in ucode configure --agents opencode output that multi-turn GPT-5 conversations are known-broken on stock OpenCode.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions