Skip to content

fix(release): drupal.org blockers + live-tested bugs + tool-use defaults#10

Open
steveworley wants to merge 2 commits into
1.xfrom
fix/release-blockers
Open

fix(release): drupal.org blockers + live-tested bugs + tool-use defaults#10
steveworley wants to merge 2 commits into
1.xfrom
fix/release-blockers

Conversation

@steveworley
Copy link
Copy Markdown
Contributor

Summary

Clears drupal.org release blockers + three bugs surfaced during live runtime testing against the QuantGov Dashboard API. Follow-up to #8 (settings-form accessibility rewrite) — opens cleanly on top of 1.x post-#8 merge (rebase pre-verified, zero conflicts).

What's fixed

Provider contract

  • LSP violation on isUsable() and getConfiguredModels() — add the array type hint required by AiProviderInterface. Fatal under PHP 8.x strict mode.
  • Add use ChatTrait; so cost-tracking + tagged-chat helpers don't fail at runtime.

Live-tested bugs (probed against https://dash.quantgov.cloud)

  • Structured output payload shape: responseFormat (camelCase) + type: "json" was rejected with 400 on every call. Now sends response_format (snake) + type: "json_schema" + flat schema / name / strict. Verified across all three call paths: buffered chat, streaming chat, and the deprecated chatStream().
  • VDB upload response parser: server returns uploadedDocuments[*].documentIds, not top-level documentIds. Without this fix the vdb_mapping state was silently never populated. Lookup-by-Drupal-ID flows now work.
  • Token-leak in error logs: AuthService was logging the first 500 chars of OAuth response bodies, and QuantCloudClient was logging Guzzle exception messages (which include the full request payload). Both now log status + reason phrase only. Verified across 30 watchdog rows post-fix: zero token strings leak.

Tool-use friendly default tokens

Packaging

  • `LICENSE.txt` (GPL-2.0-or-later, matches `composer.json`).
  • `.gitattributes` (export-ignore for tests/, .docker/, etc.).
  • `.gitignore` (vendor/, .idea/, .vscode/, .claude/, .DS_Store).
  • `composer.json`: minimum-stability stable, drupal/ai ^1.2.0, drupal/search_api ^1.36 (the VDB submodule's info.yml declared the dep but composer didn't).
  • `info.yml`: `package: 'AI Providers'`, `lifecycle: experimental` + `lifecycle_link`.

Coding standards

  • `phpcbf --standard=Drupal,DrupalPractice` swept the module: 389 of 394 errors auto-fixed. Remaining 8 errors + 29 warnings are line-too-long / deprecation-tag format / doc-comment-missing — scoped for a follow-up.

Test plan

Live runtime checks (drush ev, against QuantGov):

  • Buffered chat (Nova Lite + Claude Haiku 4.5)
  • Streamed chat / SSE iterator (38 deltas reassembled)
  • Embeddings (Titan v2 1024d, Cohere v3 English + Multilingual)
  • Multimodal (PNG describe via Claude + Nova Pro)
  • Tool use (clean schema → correct toolUseId + input)
  • Structured output (Nova Lite returns raw JSON; Claude Haiku wraps in fences — model-side compliance issue, not provider)
  • OAuth refresh (forced expiry → /oauth/token round-trip, fresh 90-day expiry persisted)
  • VDB CRUD (create / upload / queryByText / queryByVector / drop)
  • Error paths (bad model → 400, bad token → 401, no leakage)
  • `drush updb` runs `update_10003` cleanly: 16384 → 32768

Known follow-ups (Tier 2 / 3 — not in this PR)

  • Move `AuthService::validateToken()` out of `buildForm()` (per-render network round-trip).
  • Add `validateForm()` override that exercises the token at submit.
  • Use `#type => 'key_select'` with `key_type` filter (currently lists all Key entities).
  • Fix refresh-token lock race (second `acquire()` return not checked).
  • `AuthService::writeKey()` delete-then-create is non-atomic.
  • Hardcoded model→vector-dim map should fall back to API.
  • Delete deprecated `QuantCloudStreamingClient::chatStream()`.

Upstream / server-side issues (out of scope here)

  • `drupal/ai` `ToolsPropertyInput::renderPropertyArray()` emits non-standard JSON Schema fields (per-property `name`, boolean `required`) that Anthropic rejects. File upstream.
  • Quant Dashboard `/oauth/token` returns the same access_token on every refresh — only `expires_in` is fresh. Worth confirming intentional vs. token-rotation being skipped.
  • Quant Dashboard accepts `response_format` payloads but doesn't enforce the schema. Workaround: default `chat_with_structured_response` to Nova Lite, which complies natively.

🤖 Generated with Claude Code

Resolves a batch of drupal.org packaging blockers plus three bugs
surfaced during live runtime testing against the QuantGov Dashboard
API. Builds on top of PR #8 (settings-form accessibility rewrite).

Provider contract
- Fix LSP violation on QuantCloudProvider::isUsable() and
  ::getConfiguredModels(): add the `array` type hint required by
  AiProviderInterface. Previously fatal under PHP 8.x strict mode.
- Add `use ChatTrait;` so cost-tracking and tagged-chat helpers don't
  fail at runtime when callers (AI Explorer, Canvas AI agents) invoke
  them.

Live-tested bug fixes
- Structured output payload shape: send `response_format` (snake_case)
  with `type: "json_schema"` and forward `schema`, `name`, `strict`
  from the normalized wrapper. The previous `responseFormat` /
  `jsonSchema` shape returned 400 on every chat() call with a schema.
  Updated in QuantCloudProvider::chat() plus both clients
  (QuantCloudClient::chat and QuantCloudStreamingClient::chatStreamRaw
  + deprecated chatStream).
- VDB upload response parser: read documentIds from the nested
  `uploadedDocuments[*].documentIds` field instead of the flat
  top-level `documentIds` (which doesn't exist on the live API).
  Without this fix the state mapping
  `ai_provider_quant_cloud.vdb_mapping.<collection>` was silently
  never populated, breaking lookup-by-drupal-id flows.
- Token-leak scrub in error logs: stop logging OAuth response bodies
  (AuthService::exchangeCodeForToken, ::refreshToken) and Guzzle
  exception messages (QuantCloudClient::post, ::get). The OAuth body
  on a borderline 4xx can echo the token itself, and Guzzle's
  exception message includes the full request payload. Now logs
  status + reason phrase only.

Tool-use friendly default tokens
- Bump default max_tokens 16384 -> 32768 across the client constant,
  install config, and api_defaults. Gives 2x headroom over PR #8 for
  multi-round tool-use chains (Canvas AI orchestrator, Anthropic best
  practice recommends 8-16K per round; complex workflows easily hit
  30K+). Form max stays at 65536 so site builders can still configure
  higher.
- Add update_10003 hook to bump existing sites from 16384 -> 32768
  (only if their current setting is <= 16384, so custom higher values
  are preserved).

Packaging
- Add LICENSE.txt (GPL-2.0-or-later, matches composer.json).
- Add .gitattributes with export-ignore for tests/, .docker/,
  docker-compose.yml, Makefile and other dev-only files so
  `composer create-project` does not ship the eval rig.
- Add .gitignore for vendor/, .idea/, .vscode/, .claude/, .DS_Store.
- composer.json: minimum-stability stable (was dev),
  drupal/ai ^1.2.0 (was ^1.0 - older 1.x lacks ChatTrait and
  structured-output traits), drupal/search_api ^1.36 added (the VDB
  submodule's info.yml declares this dep but composer.json didn't).
- info.yml: package 'AI Providers' (was 'AI', groups with peer
  providers on the Extend page), lifecycle: experimental +
  lifecycle_link.

Coding standards
- phpcbf sweep against Drupal,DrupalPractice standard. 389 of 394
  errors auto-fixed: trailing whitespace, end-of-file newlines,
  comment-line punctuation, use-statement ordering, alphabetical
  imports, doc-comment spacing. 8 errors and 29 warnings remain
  (line-too-long, deprecation-tag format, doc-comment-missing) -
  scoped for a follow-up.

Live-verified post-fix
- Buffered chat (Nova Lite + Claude Haiku 4.5)
- Streamed chat (SSE iterator, 38 deltas)
- Embeddings (Titan v2 + Cohere v3 English + Multilingual)
- Multimodal (PNG -> Claude/Nova image description)
- Tool use (clean schema -> correct toolUseId + input)
- Structured output (Nova Lite returns raw JSON, Claude Haiku still
  wraps in fences but is a model-side compliance issue not provider)
- OAuth refresh flow (forced expiry -> /oauth/token round-trip,
  fresh expiry persisted)
- VDB CRUD (create / upload / queryByText / queryByVector / drop)
- Error paths (bad model -> 400, bad token -> 401, no token leakage
  observed in 30 watchdog rows)
- update_10003 idempotent: runs once to bump 16384 -> 32768, no-op
  on subsequent updb invocations.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@quantcode-agent quantcode-agent Bot left a comment

Choose a reason for hiding this comment

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

PR #10 Review — fix(release): drupal.org blockers + live-tested bugs + tool-use defaults

Branch: fix/release-blockers1.x | Commit: 1cc7204


Summary of Changes

This PR bundles five distinct categories of change:

  1. drupal.org release blockers.gitattributes, lifecycle: experimental, package rename, LICENSE.txt, README.md, CHANGELOG.md
  2. response_format key renameresponseFormat/jsonSchemaresponse_format/json_schema to match the actual Quant Cloud API contract
  3. Tool-use response parsing fix — dual-path extraction for toolUse from both nested (response.toolUse) and flat (toolUse) Lambda response shapes
  4. VDB document-ID path fixuploadedDocuments[0].documentIds → corrected null-coalesce path
  5. max_tokens bump — 16 384 → 32 768 via update_10003, plus a new logPotentialChatFailure() diagnostic helper
  6. ChatTrait adoption — drops the manual $chatSystemRole property in favour of the base-class trait; isUsable() type-hint tightened to array; AiModelCapability enum used for capability comparison

Blockers

None. No exploitable security vulnerabilities, data-loss paths, or ISM control violations were introduced by this diff.


Warnings

W1 — QuantCloudStreamingClient import removed but class still injected at runtime
src/Plugin/AiProvider/QuantCloudProvider.php

The use statement for QuantCloudStreamingClient was removed from the imports, but the class is still referenced in the body via @var docblock and create() factory (using the FQCN string). PHP won't fatal at runtime, but removing the use statement while keeping the FQCN in a docblock is misleading and will cause IDE type-inference to break. If a future refactor adds a type-hint or instanceof check against the short class name, it will fail silently. The use statement should be restored, or the docblock updated consistently.


W2 — response_format.strict defaults to FALSE — may silently produce non-conforming JSON
src/Plugin/AiProvider/QuantCloudProvider.php (~line 287)

$options['response_format'] = [
    'type'   => 'json_schema',
    'schema' => $schema['schema'],
    'name'   => $schema['name'] ?? 'json_schema',
    'strict' => $schema['strict'] ?? FALSE,   // ← defaults to FALSE
];

When strict is FALSE the model is not required to conform to the supplied schema; it may return additional properties or omit required ones. For a government platform where downstream code deserialises the response into typed objects, a silent schema violation is a data-integrity risk. The safer default is TRUE, with callers explicitly opting out. At minimum, document why FALSE is the chosen default.


W3 — update_10003 silently skips all configs if listAll() returns nothing
ai_provider_quant_cloud.install

If listAll() returns an empty array (e.g. config stored in DB but config factory bootstrapped before DB is available during drush updb), the update silently succeeds without touching any config. There is no post-condition check or log message. Consider logging the count of updated configs, or asserting at least one was found.


W4 — logPotentialChatFailure() logs raw $content at WARNING level unconditionally
src/Plugin/AiProvider/QuantCloudProvider.php (~line 432–480)

if ($tools_requested && !$tool_use_data) {
    $this->logger->warning(
        'Tool-use requested but no tool call returned. ...',
        ['@content' => print_r($content, TRUE), ...]
    );
}

$content is the raw model response text. If the model returns a long conversational reply (valid when the model decides no tool is needed), the full text is dumped into Drupal watchdog at WARNING level. On a busy site this will bloat the watchdog table and may inadvertently log user-supplied content or PII that appeared in the conversation. Truncate $content to a safe length (e.g. 500 chars) before logging, and consider whether WARNING is the right severity for a valid "model chose not to call a tool" scenario (NOTICE may be more appropriate).


W5 — VDB document-ID path fix is correct but fallback is an empty array with no error signal
modules/ai_provider_quant_cloud_vdb/src/Plugin/VdbProvider/QuantCloudVdbProvider.php (line 448)

$document_ids = $response['uploadedDocuments'][0]['documentIds'] ?? [];

If the API returns an unexpected shape, $document_ids silently becomes [] and the caller receives no indication the upload may have failed. A log warning or exception when $document_ids is empty after a nominally successful upload would make failures observable.


Nits (non-blocking)

  • ai_provider_quant_cloud.info.yml: lifecycle_link URL points to https://www.drupal.org/node/3369575 — worth verifying this is the correct node for the experimental lifecycle definition.
  • QuantCloudProvider.php: Several inline comments still end without a full stop after this PR — minor consistency issue.
  • ai_provider_quant_cloud.module: The hook_help() output still uses concatenated $output .= rather than a render array. Not introduced by this PR, but worth noting for future cleanup.
  • CHANGELOG.md / README.md: Good additions for drupal.org release hygiene.

What's Done Well

  • AiModelCapability enum adoption — comparing $capability->value against the API string avoids subtle type-mismatch bugs present in the base branch.
  • ChatTrait adoption removes a hand-rolled $chatSystemRole property that duplicated base-class state.
  • Dual-path toolUse extraction correctly handles both the nested Lambda wrapper format and flat format, with a clear comment explaining the two shapes.
  • isUsable() type-hint tightened from untyped to array $capabilities — small but correct.
  • .gitattributes correctly excludes dev artefacts from drupal.org release archives.
  • update_10003 iterates all provider configs rather than hard-coding a single config name — correct for multi-instance setups.

Overall Risk Assessment

The three live-tested bug fixes (response_format key rename, toolUse dual-path extraction, VDB document-ID path) are all straightforward and correct. The drupal.org release-blocker changes are mechanical. The main risks are the silent failure modes in W3 and W5, the PII-in-logs risk in W4, and the misleading removed import in W1. None are merge-blocking on their own, but W4 (logging raw model content) is worth addressing before a public drupal.org release given the government/PROTECTED data context this module operates in.


Verdict: COMMENT — no blockers; the fixes are correct and the release-blocker changes are appropriate. Address W4 (truncate logged content) and W1 (restore the QuantCloudStreamingClient import) before the drupal.org release tag.

- W1: restore the QuantCloudStreamingClient `use` statement that was
  dropped in 1cc7204. The class is still referenced via docblock and
  resolved from the container, but the missing import broke IDE
  type-inference and was misleading.
- W5: log a warning when document upload returns an empty
  `documentIds` array. Previously the state-mapping fell through
  silently if the API response shape changed, leaving operators
  without any signal that the upload may have partially failed.

W2 (response_format.strict default), W3 (update_10003 listAll), and
W4 (logging raw content) were false alarms on the reviewer's part —
W2 matches the OpenAI base class default, W3 doesn't use listAll(),
and W4's logger never includes the raw content placeholder.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.

1 participant