Skip to content

fix: send valid tool schemas and non-empty reasoning replay to OpenAI-compatible relays - #900

Merged
vastsa merged 2 commits into
mainfrom
fix/tool-schema-required
Sep 22, 2026
Merged

vastsa merged 2 commits into
mainfrom
fix/tool-schema-required

Conversation

@vastsa

@vastsa vastsa commented Sep 22, 2026

Copy link
Copy Markdown
Owner

Fixes #864.

Two independent causes behind PROVIDER_ERROR against an OpenAI-compatible
relay (ps.air-outer.com + deepseek-v4-flash).

1. Read reached the relay with no required key at all.
Read, Glob, Grep, and BrowserPreview declare their canonical argument
as Type.Optional so the file_path / query alias spelling validates too;
TypeBox 1.x then omits required entirely. The OpenAI chat/completions adapter
forwards parameters verbatim (only the Anthropic adapter fills
required ?? []), so a relay that decodes the missing key into a nil slice
answered Invalid schema for function 'Read': null is not of type "array".
Neither the relay nor PI-Desktop ever emitted a literal null.

The schema is now normalised in rebuildToolCatalog(), the single origin of
every tool, so the session agent and a delegated Task run send the same
declaration. An absent required already means "nothing is required", so
writing an empty array changes no tool's accepted arguments.

2. The non-empty reasoning replay never reached the wire.
The 0.86.1 patch taught convertMessages to fill a documented placeholder for
relays that reject an empty reasoning echo, but pi-ai rebuilds compat from an
explicit allowlist in getCompat, so the flag provider-binding puts on a
relayed DeepSeek model was dropped. The fill silently degraded to an empty
string, which strict relays reject with The reasoning_content in the thinking mode must be passed back to the API. The opt-in now passes through getCompat
and is declared on OpenAICompletionsCompat. No endpoint can be auto-detected
as requiring a non-empty echo, so it stays an explicit model.compat opt-in.

Validation

Base: 6ad892d806af

  • pnpm build:js — passed (including apps/desktop)
  • pnpm --filter @pi-desktop/desktop typecheck — passed
  • pnpm -r --if-present test — passed
  • pnpm lint — passed
  • node scripts/check-architecture.mjs — passed (2 new files, 40 / 52 LOC)
  • pnpm check:pr-base — passed
  • Rust untouched

Each new test was confirmed to fail before its fix:

  • Read.required absent: Read: expected { Object (type, properties) } to match object { type: 'object', required: [] } on both the session and the
    delegate path
  • getCompat passthrough removed: expected '' to be '[reasoning not retained for this turn]'

The reasoning test drives a real request through buildProviderModel → pi-ai
getCompat → the adapter and asserts the captured outbound body, because the
existing backfill test hands a hand-built compat straight to
convertMessages and therefore cannot see a dropped flag.

Not run: verify:ui:* E2E. These are wire-format defects proven at the adapter
boundary — the reasoning test asserts the actual request body — and the UI E2E
suites require an explicit request per repository policy.

TypeBox 1.x omits `required` altogether when no property is required, and
Read, Glob, Grep, and BrowserPreview declare their canonical argument as
optional so the `file_path` / `query` alias spelling validates too. The
OpenAI chat/completions adapter forwards `parameters` verbatim — only the
Anthropic adapter fills `required ?? []` — so a relay that decodes the
missing key into a nil slice answered `Invalid schema for function 'Read':
null is not of type "array"` (#864).

Normalise the schema where the tool catalogue is built, because that is the
single origin of every tool: the session agent and a delegated Task run then
send the same declaration. An absent `required` already means "nothing is
required", so writing `[]` states the same contract explicitly and changes
no tool's accepted arguments.
The 0.86.1 patch taught convertMessages to fill a documented placeholder
when a DeepSeek-family relay rejects an empty reasoning echo, but pi-ai
rebuilds compat from an explicit allowlist in getCompat. The flag that
provider-binding puts on a relayed DeepSeek model never reached the adapter,
so the fill silently degraded to "" and the relay rejected the request with
`The reasoning_content in the thinking mode must be passed back to the API`
(#864).

Pass the opt-in through getCompat and declare it on OpenAICompletionsCompat.
No endpoint can be auto-detected as requiring a non-empty echo, so
detectCompat reports it as undefined and only an explicit model.compat
opt-in enables it.

fixes #864
Copilot AI lite review requested due to automatic review settings September 22, 2026 18:37

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@vastsa
vastsa merged commit 6c99c02 into main Sep 22, 2026
3 checks passed
@vastsa
vastsa deleted the fix/tool-schema-required branch September 24, 2026 01:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] 使用 ps.air-outer.com 调用 deepseek-v4-flash 时出现协议兼容错误

2 participants