WIP: feat(ai): support both responses and chat-completions APIsfeat/ai-api-type #163
Draft
jabber.developer2
wants to merge 4 commits from
feat/ai-api-type into master
pull from: feat/ai-api-type
merge into: devs:master
devs:master
devs:fix/editor-terminal-size
devs:ci/docker-hub-publishing
devs:feat/disco-ac
devs:feat/privacy-enhancements
devs:fix/clientid-regression
devs:fix/ai-chat-completions-followup
devs:feat/ai-custom
devs:fix/unencrypted-send
devs:rollback/pre-upstream-merge
devs:fix/autoping-warning-null-domain
devs:fix/pad-dead-space-reclaim
devs:feat/autoping-warning
devs:chore/untrack-gitversion
devs:fix/multiline-pad-clip
devs:fix/issue-112-followups
devs:fix/issue-128-migrate-v3-dedup
devs:fix/delay-timestamp-validation
devs:ref/light-cleanup
devs:fix/history-scroll-pad-redraw-storm
devs:fix/plugin-post-display-incoming-only
devs:merge/upstream-full
devs:merge-improve
devs:chore/remove-chatlog-stage-1
devs:fix/ai-json-encoding
devs:feat/no-db-backlog-114
devs:fix/scroll-non-chat-windows
devs:fix/paged-non-chat-windows
devs:fix/ai-leaks
devs:feat/ai
devs:fix/ai-followups
devs:test/ai-coverage-unit-only
devs:test/ai-coverage
devs:refactor/scroll-mechanism
devs:fix/verify-per-contact-context
devs:feat/no-db-mode
devs:feat/ai-json
devs:feat/pikaur-parity-arch
devs:fix-pikaur-build
devs:feat/upstream-sync
devs:feat/functest-speedup
devs:fix/cwe-134-format-string-audit
devs:ci/separate-build-step
devs:test/autoping-functional-tests
devs:test/db-functional-tests
devs:fix/arch-build
devs:fix/xep-0030-disco-items-error-handling
devs:fix/xep-0030-empty-disco-items
devs:playground/fix/src_refactoring
devs:tests/disco
devs:fix/test-CI-stability
devs:feat/parallel-tests-clean
devs:feat/parallel-functional-tests
devs:fix/functional_tests_v2
devs:fix/functional_tests
devs:fix/connect_max_args
devs:feat/extended_debug_info
devs:playground/fix/scroll-stuck
devs:build/multicore
devs:build/reenable-fedora
devs:build/autoupdate
No Reviewers
Labels
Clear labels
Compat/Breaking
Breaking change that won't be backward compatible
Kind/Bug
Something is not working
Kind/Documentation
Documentation changes
Kind/Enhancement
Improve existing functionality
Kind/Feature
New functionality
Kind/Security
This is security issue
Kind/Testing
Issue or pull request related to testing
Priority
Critical
1
The priority is critical
Priority
High
2
The priority is high
Priority
Low
4
The priority is low
Priority
Medium
3
The priority is medium
Reviewed
Confirmed
1
Issue has been confirmed
Reviewed
Duplicate
2
This issue or pull request already exists
Reviewed
Invalid
3
Invalid issue
Reviewed
Won't Fix
3
This issue won't be fixed
Status
Abandoned
3
Somebody has started to work on this but abandoned work
Status
Blocked
1
Something is blocking this issue or pull request
Status
Need More Info
2
Feedback is required to reproduce issue or to continue work
No Label
Milestone
No items
No Milestone
Projects
Clear projects
No project
No Assignees
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: devs/cproof#163
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
No description provided.
Delete Branch "feat/ai-api-type"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
The chat-completions alignment (
9913344bb) fixed the customer's chat-completions-only provider but broke Perplexity, which serves only the responses API. This PR drives both flavours from one code path: requests default to/v1/responsesand fall back to/v1/chat/completionswhen the provider reports the endpoint missing or rejects the payload shape, plus a manual per-provider override via/ai set api-type <provider> responses|chat-completions|auto.Design
AIApiTypeenum (auto/responses/chat-completions) on the provider:api_typeis the configured value (persisted asapi_type=in[ai/<provider>]),resolved_api_typeis a runtime-only cache of the detected flavour, andapi_epochis a generation counter bumped on any URL or api-type change so a stale in-flight detection cannot overwrite a newer decision.Request flow: an explicitly pinned flavour is used as-is. In
auto, the request tries the preferred flavour first and falls back to the other on an endpoint-missing status (404/405/501) or a payload-shape rejection (400/422). A previously detected flavour is treated as a first-attempt hint, not a hard pin — the other flavour stays available as a fallback, so a backend that changes flavour self-corrects within the request instead of wedging on the cache.One request, one prompt: history and custom settings are serialized once per request and the per-flavour envelope is assembled per attempt under a single session-lock acquisition, so the fallback retry cannot double-count the prompt.
Response parsing (
ai_parse_response_typed()): dispatch on the request flavour, then per-envelope extractors — chat completions anchored onchoices[].message.content; responses on theoutput_textpart with a string-aware backward"text"scan bounded to that part's own JSON object, so a reasoning summary or a truncated body is never returned as the assistant reply.ai_parse_response()is theAUTOentry point over the same ordered pipeline (no extractor runs twice).Endpoint detection: wrong-model errors are classified from the structured
error.code/type/messagewhen a JSON error envelope is present (and a bare "does not exist" must mention a model), so route-missing 404s from gateways/proxies no longer suppress the fallback. When both flavours fail, the first payload-rejection error (the actionable 400/422) is preserved and surfaced instead of the second endpoint's bare 404; an unparseable 2xx re-probes.Thread safety:
api_urlis snapshotted undersettings_lockin both the request and models-fetch threads, and everyresolved_api_typewrite goes through one locked helper with an epoch recheck — closing a use-after-free and a stale-cache race against a concurrent/ai set provider. The models-fetch provider reference is taken on the main thread (while the provider is guaranteed live) and released on every worker exit path.Reserved custom-setting keys extended with
inputso/ai set customcannot collide with the responses payload;storestays writable as a legitimate chat-completions parameter.UX:
/ai set api-typewith provider-name and value autocompletion;/aiand/ai providersshow the configured type, orauto (detected: …)once probed; help/synopsis/examples updated.Commits
14f76ab1dfeat(ai): support both responses and chat completions APIs — the whole feature as a single unit (enum + provider fields, the attempt loop with fallback, the parameterized payload builder, the dual-format parser, the epoch/lock protocol, persistence, command wiring and tests). Touchessrc/ai/ai_client.{c,h},src/config/preferences.{c,h},src/command/{cmd_funcs,cmd_defs,cmd_ac}.c,tests/unittests/.b3a269342fix(ai): repair custom setting name autocompletion — an independent pre-existing bug (below), kept separate because it does not touch the dual-API path.Follow-up commit: custom setting name autocompletion (
b3a269342)The
/ai set custom <provider> <setting>name completion never worked since its introduction in9e5dfb14f: the prefix"/ai set custom "carried a trailing space thatautocomplete_param_with_ac()doubled, the chosen primitive completes the first token after the command while the setting name is the second, thenum_args == 3guard excluded partially typed names, and the static list (tools/search/memory/plugins) predated the backend. Replaced with token-position completion (autocomplete_param_no_with_func, arg 5) over a list rebuilt on demand: common payload parameters plus the provider's currently set keys via the newai_get_provider_setting_keys()(snapshot undersettings_lock). The unusedparse_args()call whose NULL result hitg_strv_length()unguarded (glib CRITICAL on TAB at/aior at the 5-arg value position) is gone with the dead branch.Testing
gcc -fsyntax-onlyclean on all touched translation units; fullmake/make checknot run yet — please run before merge.Behavioural coverage is unit-level (parser incl. reasoning-leak / brace-in-text / logprobs-before-content / literal-
"text"regressions, settings, persistence). The fallback path still needs a live smoke test against OpenAI (both endpoints), Perplexity (responses-only), and the customer's chat-completions-only provider.Notes / open questions
The original spec (2026-07-04): default to
responses; on 404, fall back tochat/completions; plus a manual/ai set api-type MY_PROVIDER chat-completions|responses. Implemented as specified, with one deviation to confirm: the fallback also triggers on a payload-shape rejection (400/422), not only on 404. Rationale: a flavour-specific custom setting or envelope shape would otherwise wedge every request with no recovery. If you'd rather keep the fallback strictly 404-only, this is a one-line narrowing of the endpoint-missing predicate.The fallback probe costs one extra round-trip only when the first-attempt flavour is wrong; once a flavour answers it is reused as the hint for the rest of the session (not persisted across restarts).
Parser: - Never return reasoning text as the reply: the no-output_text path anchors strictly on the message "content" array; a reasoning-only body (truncated by max_output_tokens, carries only "summary") now fails the parse instead of leaking the chain-of-thought. Regression test added. - _find_json_field skips field-name occurrences that are string values (no ':' after), so {"type":"text","text":"hi"} parses instead of failing on the value of "type". - _same_json_object tracks brace depth, so a nested object between the "text" key and the "output_text" tag (e.g. "annotations") no longer rejects the part's own text. - Chat-completions extraction is bounded to the choices array (new _json_array_end helper), so "message"/"content" in a sibling object (e.g. a top-level "warning") is not returned as the reply. AUTO fallback: - Model errors are classified only from a structured JSON error envelope; a route-missing 404 page merely mentioning models (e.g. listing /v1/models) no longer suppresses the fallback. Trade-off: a model error in a non-JSON body costs one extra fallback attempt. - Fall back on a curl-level failure: an endpoint that accepts the connection but never responds no longer burns the 60s timeout without trying the flavour that works. - Fall back on an unparseable 2xx: a gateway answering unknown paths with a 200 error page previously reset the hint and re-probed the same dead flavour on every request. The body is parsed inside the attempt loop and no longer re-parsed after it. - The second attempt's 401/403/429/5xx now wins over the stashed 400/422 payload rejection, so an invalid key or rate limit is not hidden behind a payload-shape error; a second 404 still defers to the first. - When the first flavour's endpoint is missing but the other answers (even unsuccessfully, e.g. 401), the resolved hint moves to the surviving flavour, so the dead endpoint is not probed first forever. Settings: - Reserved payload keys are rejected only when setting a value; removal is allowed again, so a setting persisted before its key became reserved (e.g. "input") is no longer stuck in the config.- bound _parse_responses scans to the output_text part's object and to the content array, so a later sibling item's "text" (e.g. a reasoning summary) can never be returned as the assistant reply - do not retry the other flavour on a curl timeout: the request likely reached the server and may still be generating, so a re-POST of the conversation could trigger a second billed generation - remember an unparseable 2xx from the first AUTO attempt and surface stashed first-attempt errors in both error paths, instead of showing only the final attempt's transport or HTTP error - recognize Ollama's model-not-found wording in _names_model so a model typo is not misread as a missing endpoint - drop the dead, racy provider-lookup fallback in the generic request thread: a missing provider ref is a caller bug and now fails loudly - fix ai_providers_lists_defaults to expect the header the command actually prints ("Configured providers:"); the test was broken since its introduction but CI never ran it - add functional test group 5 (AI command surface) to FUNC_TEST_GROUPS so the CI parallel target runs it; proftest.c port ranges already account for five groupse9614abb89to3f430e01d23f430e01d2tof7a818dc1df7a818dc1dto37218d2e07View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.